import csv
import itertools
import math
import numpy as np
import numpy.typing as npt


class TexnoMagicDrawing:
    """TexnoMagic Drawing is a set of 2D curves defined by points.

    Specifically, Drawing is list of lists of 2D points (a list of separated
    curves) as generated by pointing devices such as mouse, touch, and tablets.

    This abstraction is roughly equivalent to using a pen & paper.
    Individual drawing points indicate the trajectory of a
    pen/pencil/marker/brush/chalk/etc when it was writing.

    Drawing is a usually a representation of a
    [Symbol][texnomagic.symbol.TexnoMagicSymbol].

    Drawings are stored as CSV files with individual curves separated
    by empty lines (`,`).

    `self.path` is a path of Drawing data CSV file.

    This class provides convenient utilities for working with Drawings,
    see individual methods.
    """

    def __init__(self, path=None, curves=None, points_range=1000.0):
        self.path = path
        self.points_range = points_range
        self._curves = None
        self._points = None
        self._file_size = None
        if curves:
            self.set_curves(curves)

    @property
    def curves(self) -> list[np.array]:
        """Individual curves (lists of points).

        Lazy loaded on-demand."""
        if self._curves is None:
            self.load_curves()
        return self._curves

    @property
    def points(self) -> np.array:
        """All points from all curves.

        Lazy loaded on-demand."""
        if self._points is None:
            self.load_curves()
        return self._points

    @property
    def name(self) -> str | None:
        """Drawing file name.

        Derived from self.path."""
        if self.path:
            return self.path.name
        return None

    @property
    def file_size(self) -> int:
        """Drawing file size.

        Lazy loaded on-demand."""
        if self._file_size is None:
            self._file_size = self.path.stat().st_size
        return self._file_size

    def set_curves(self, curves):
        """Assign curves.

        Converts to a single numpy.array points with curves being views
        into the array for fast processing."""
        # keep all points in single continuous numpy array
        self._points = np.array(list(itertools.chain(*curves)), dtype=np.float64)
        self._curves = []
        i = 0
        for curve in curves:
            n = len(curve)
            # curves are numpy views into main points array
            cview = self._points[i:i+n]
            self._curves.append(cview)
            i += n

    def load(self, path=None):
        # this is only kept for consistence with symbol and abc
        if path:
            self.path = path
        return self

    def load_curves(self):
        """Load Drawing curves from file."""
        curves = []
        curve = []
        with self.path.open('r') as f:
            reader = csv.reader(f)
            for row in reader:
                if not row or '' in row:
                    # empty rows separate individual curves
                    curves.append(curve)
                    curve = []
                    continue
                point = list(map(float, row[:2]))
                curve.append(point)
        curves.append(curve)
        self.set_curves(curves)

    def save(self):
        """Save drawing to CSV file specified by self.path."""
        self.path.parent.mkdir(parents=True, exist_ok=True)
        with self.path.open('w', newline='') as f:
            writer = csv.writer(f)
            first = True
            for curve in self.curves:
                if first:
                    first = False
                else:
                    # curves separator
                    writer.writerow([None, None])
                writer.writerows(curve.tolist())

    def normalize(self):
        """
        Normalize drawing points in-place into <0, self.points_range> range.

        See also [curves_fit_area][texnomagic.drawing.TexnoMagicDrawing.curves_fit_area].
        """
        if len(self.points) == 0:
            return

        # move to [0,0]
        self._points -= np.min(self.points, axis=0)
        # normalize
        k = self.points_range / np.max((np.max(np.max(self._points, axis=0)), 0.2))
        self._points *= k
        # center
        offset = (self.points_range - np.max(self._points, axis=0)) / 2
        self._points += offset

    def curves_fit_area(self, pos : tuple[float, float] | npt.ArrayLike, size : tuple[float, float] | npt.ArrayLike) -> list[npt.ArrayLike]:
        """
        Return curves scaled to fit area.

        Useful for drawing curves in UI

        See also [normalize][texnomagic.drawing.TexnoMagicDrawing.normalize].

        Args:
          pos: position / offset (x, y) - relative <0, 1>
          size: desired size (width, height) - relative <0, 1>

        Returns:
          Curves scaled to fit desired area.
        """
        pos = np.array(pos)
        size = np.array(size)

        k = np.min(size) / self.points_range
        max_range = self.points_range * k

        offset = pos + (size - max_range) / 2

        scurves = []
        for curve in self.curves:
            if len(curve) > 0:
                scurve = curve * k + offset
            else:
                scurve = curve
            scurves.append(scurve)
        return scurves

    def flip_y_axis(self):
        """
        Flip Drawing along Y axis in-place.

        Useful for compatibility with systems that use different Y axis sign.
        """
        self._points[:,1] = self.points_range - self._points[:,1]

    def delete(self):
        """Delete the Drawing file."""
        if not self.path or not self.path.exists():
            return
        self.path.unlink()

    def pretty(self, size=True) -> str:
        """Pretty Drawing string with colors in rich formatting."""
        n_curves = len(self.curves)
        n_points = len(self.points)
        s = f"[white bold]{self.path.name}[/]: "
        s += f"{n_points} points, {n_curves} curves"
        if size:
            fsize = math.ceil(self.file_size / 1024.0)
            s += f", {fsize} kB"
        return s

    def __str__(self) -> str:
        if self._curves is None:
            info = "curves not loaded"
        else:
            info = f"{len(self._points)} points, {len(self._curves)} curves"
        return f"{self.name}: {info}"

    def __repr__(self) -> str:
        return '<TexnoMagicSymbol %s>' % self.__str__()
