#!/usr/bin/env python3

#pylint: disable=too-many-instance-attributes

import argparse
import math
from copy import copy

import drawSvg as draw


class Part:
    """
    Part of goban
    """
    def __init__(self, stones, pos, stonesOffset, config):
        self.stones = stones
        self.pos = pos
        self.stonesOffset = stonesOffset
        self.config = config
        self.scale = config.resolution
        self.size = [
            (self.stones[0]-1)*config.cellsize[0],
            (self.stones[1]-1)*config.cellsize[1]
        ]

        borders_map = ((3, 1), (0, 2))

        total_stones = [
            stonesOffset[0] + self.stones[0],
            stonesOffset[1] + self.stones[1]
        ]

        self.is_first = [
            self.pos[0] == 0,
            self.pos[1] == 0
        ]
        self.is_last = [
            total_stones[0] == config.size[0],
            total_stones[1] == config.size[1]
        ]

        for axis in (0, 1):
            if self.is_first[axis]:
                self.size[axis] += config.borders[borders_map[axis][0]]
                self.size[axis] += config.stone_radius + config.stone_margin
            else:
                self.size[axis] += config.cellsize[axis]/2

            if self.is_last[axis]:
                self.size[axis] += config.borders[borders_map[axis][1]]
                self.size[axis] += config.stone_radius + config.stone_margin
            else:
                self.size[axis] += config.cellsize[axis]/2

        self.offset = [0, 0]
        for axis in (0, 1):
            if self.is_first[axis]:
                self.offset[axis] += config.borders[borders_map[axis][0]]
                self.offset[axis] += config.stone_radius + config.stone_margin
            else:
                self.offset[axis] += config.cellsize[axis]/2

        self.size = [size*self.scale for size in self.size]
        self.offset = [offset*self.scale for offset in self.offset]

        self.recto_drawing = draw.Drawing(*self.size)
        self.verso_drawing = draw.Drawing(*self.size)
        self.base_drawing = draw.Drawing(*self.size)

    def is_hoshi(self, pos: list):
        """
        Is this position a hoshi ?
        """
        size = self.config.size
        pos = [
            pos[0] + self.stonesOffset[0],
            pos[1] + self.stonesOffset[1]
        ]
        for axis in (0, 1):
            if size[axis] > 10:
                if pos[axis] not in (3, round(size[axis]/2)-1, size[axis]-4):
                    return False
            elif pos[axis] not in (2, round(size[axis]/2)-1, size[axis]-3):
                return False

        return True

    def is_engraved(self, pos: list) -> bool:
        """ Is the stone at pos engraved ? """
        done = self.stonesOffset[0] + pos[0]
        done *= self.config.size[0]
        done += self.stonesOffset[1] + pos[1]
        return done < self.config.engraved_stones

    def drawEngraving(self) -> None:
        """
        Draw engraving of stones and goban
        """
        cellsize = [s*self.scale for s in self.config.cellsize]
        radius = self.config.stone_radius * self.scale
        margin = self.config.stone_margin * self.scale
        thickness = self.config.thickness * self.scale
        mark_thickness = self.config.mark_thickness * self.scale
        color = self.config.engrave_color
        for x in range(self.stones[0]):
            for y in range(self.stones[1]):
                # pos(recto_x, recto_y, verso_x, verso_y)
                pos = (
                    self.offset[0]+x*cellsize[0],
                    self.offset[1]+y*cellsize[1],
                    self.size[0]-self.offset[0]-x*cellsize[0],
                    self.offset[1]+y*cellsize[1]
                )
                # Draw grid on recto
                if x > 0:
                    self.recto_drawing.append(
                        draw.Line(
                            pos[0]-radius-margin,
                            pos[1],
                            pos[0]-cellsize[0]+radius+margin,
                            pos[1],
                            stroke=color,
                            stroke_width=thickness
                        )
                    )
                if y > 0:
                    self.recto_drawing.append(
                        draw.Line(
                            pos[0],
                            pos[1]-radius-margin,
                            pos[0],
                            pos[1]-cellsize[1]+radius+margin,
                            stroke=color,
                            stroke_width=thickness
                        )
                    )
                if x == 0 and not self.is_first[0]:
                    self.recto_drawing.append(
                        draw.Line(
                            pos[0]-radius-margin,
                            pos[1],
                            pos[0]-cellsize[0]/2,
                            pos[1],
                            stroke=color,
                            stroke_width=thickness
                        )
                    )
                if y == 0 and not self.is_first[1]:
                    self.recto_drawing.append(
                        draw.Line(
                            pos[0],
                            pos[1]-radius-margin,
                            pos[0],
                            pos[1]-cellsize[1]/2,
                            stroke=color,
                            stroke_width=thickness
                        )
                    )
                if x == self.stones[0]-1 and not self.is_last[0]:
                    self.recto_drawing.append(
                        draw.Line(
                            pos[0]+radius+margin,
                            pos[1],
                            pos[0]+cellsize[0]/2,
                            pos[1],
                            stroke=color,
                            stroke_width=thickness
                        )
                    )
                if y == self.stones[1]-1 and not self.is_last[1]:
                    self.recto_drawing.append(
                        draw.Line(
                            pos[0],
                            pos[1]+radius+margin,
                            pos[0],
                            pos[1]+cellsize[1]/2,
                            stroke=color,
                            stroke_width=thickness
                        )
                    )
                # Draw engraving on stones (recto and verso)
                if self.is_engraved([x, y]):
                    engraved_radius = mark_thickness
                    while engraved_radius < radius:
                        self.recto_drawing.append(
                            draw.Circle(
                                pos[0],
                                pos[1],
                                engraved_radius - mark_thickness/2,
                                fill='none',
                                stroke=color,
                                stroke_width=mark_thickness
                            )
                        )
                        self.verso_drawing.append(
                            draw.Circle(
                                pos[2],
                                pos[3],
                                engraved_radius - mark_thickness/2,
                                fill='none',
                                stroke=color,
                                stroke_width=mark_thickness
                            )
                        )
                        engraved_radius += 2*mark_thickness

                # Draw Hoshi on base
                if self.is_hoshi((x, y)):
                    self.base_drawing.append(
                        draw.Circle(
                            pos[0],
                            pos[1],
                            radius/3,
                            fill='none',
                            stroke=color,
                            stroke_width=thickness
                        )
                    )

    def drawCutting(self) -> None:
        """
        Draw cutting of stones and goban
        """
        corner_radius = min(self.config.borders)*self.scale / 2
        cellsize = [s*self.scale for s in self.config.cellsize]
        radius = self.config.stone_radius * self.scale
        margin = self.config.stone_margin * self.scale
        thickness = self.config.cut_thickness * self.scale
        color = self.config.cut_color
        corners = [ # x, y, x, y, start, end
            (0, 0, 0, 0, 0, 0),
            (self.size[0], 0, self.size[0], 0, 0, 0),
            (self.size[0], self.size[1], self.size[0], self.size[1], 0, 0),
            (0, self.size[1], 0, self.size[1], 0, 0)
        ]

        # Cut corners on recto and base
        for drawing in (self.base_drawing, self.recto_drawing):
            if not self.config.no_round_corners:
                if self.is_first[0] and self.is_first[1]:
                    corners[0] = (
                        corner_radius, 0,
                        0, corner_radius,
                        180, 270
                    )

                if self.is_last[0] and self.is_first[1]:
                    corners[1] = (
                        self.size[0]-corner_radius, 0,
                        self.size[0], corner_radius,
                        270, 0
                    )

                if self.is_first[0] and self.is_last[1]:
                    corners[3] = (
                        corner_radius, self.size[1],
                        0, self.size[1]-corner_radius,
                        90, 180
                    )

                if self.is_last[0] and self.is_last[1]:
                    corners[2] = (
                        self.size[0]-corner_radius, self.size[1],
                        self.size[0], self.size[1]-corner_radius,
                        0, 90
                    )

        # Cut perimeter
        for (idx, corner) in enumerate(corners):
            for drawing in (self.base_drawing, self.recto_drawing):
                axis = 2 if idx%2==0 else 0
                drawing.append(
                    draw.Line(
                        corners[idx-1][0+axis],
                        corners[idx-1][1+axis],
                        corner[0+axis],
                        corner[1+axis],
                        stroke=color,
                        stroke_width=thickness
                    )
                )
                if corner[4] != corner[5]:
                    drawing.append(
                        draw.Arc(
                            corner[0],
                            corner[3],
                            corner_radius,
                            corner[4],
                            corner[5],
                            stroke=color,
                            stroke_width=thickness,
                            fill='none'
                        )
                    )

        # Cut stones on recto and holes on base
        for x in range(self.stones[0]):
            for y in range(self.stones[1]):
                # pos(recto_x, recto_y)
                pos = (
                    self.offset[0]+x*cellsize[0],
                    self.offset[1]+y*cellsize[1],
                    self.size[0]-self.offset[0]-x*cellsize[0],
                    self.offset[1]+y*cellsize[1]
                )

                self.recto_drawing.append(
                    draw.Circle(
                        pos[0],
                        pos[1],
                        radius+margin,
                        fill='none',
                        stroke=color,
                        stroke_width=thickness
                    )
                )
                self.recto_drawing.append(
                    draw.Arc(
                        pos[0],
                        pos[1],
                        radius,
                        90+math.acos(0.75)*360/(2*math.pi),
                        90-math.acos(0.75)*360/(2*math.pi),
                        fill='none',
                        stroke=color,
                        stroke_width=thickness
                    )
                )
                self.recto_drawing.append(
                    draw.Arc(
                        pos[0],
                        pos[1]+3*radius/2,
                        radius,
                        270-math.acos(0.75)*360/(2*math.pi),
                        270+math.acos(0.75)*360/(2*math.pi),
                        fill='none',
                        stroke=color,
                        stroke_width=thickness
                    )
                )
                self.base_drawing.append(
                    draw.Circle(
                        pos[0],
                        pos[1],
                        radius/4,
                        fill='none',
                        stroke=color,
                        stroke_width=thickness
                    )
                )

    def save(self) -> None:
        """
        Save drawing.
        """
        prefix = self.config.prefix
        suffix = f'-{self.pos[0]}-{self.pos[1]}'
        self.recto_drawing.saveSvg(prefix+'recto'+suffix+'.svg')
        self.verso_drawing.saveSvg(prefix+'verso'+suffix+'.svg')
        self.base_drawing.saveSvg(prefix+'base'+suffix+'.svg')


class GobanDraw:
    """
    Drawing a goban and its stones.

    @param config: Config from argparse
    """
    def __init__(self, config) -> None:
        self.config = config
        self.parts = []

        stones = list(config.size)
        part_stones = [0, 0]
        offset = [0, 0]
        x = 0
        y = 0

        while stones[0] > 0:
            stones[1] = config.size[1]
            offset[1] = 0
            y = 0
            while stones[1] > 0:
                part_stones = (
                    min(config.max_part_size[0], config.size[0]-offset[0]),
                    min(config.max_part_size[1], config.size[1]-offset[1])
                )
                self.parts.append(
                    Part(
                        part_stones,
                        (x, y),
                        copy(offset),
                        config
                    )
                )
                offset[1] += part_stones[1]
                stones[1] -= part_stones[1]
                y += 1

            offset[0] += part_stones[0]
            stones[0] -= part_stones[0]
            x += 1

        for part in self.parts:
            print(f"Part size: {part.size[0]}x{part.size[1]}")

            if not self.config.no_engrave:
                part.drawEngraving()

            if not self.config.no_cut:
                part.drawCutting()

    def save(self):
        """ Save all sketches """
        for part in self.parts:
            part.save()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Create goban and stones for lasercut"
    )
    parser.add_argument(
        '--size', type=int, nargs=2, default=(19, 19),
        help="Size of goban (number of stones)", metavar=('X', 'Y')
    )
    parser.add_argument(
        '--max-part-size', type=int, nargs=2, default=(19, 19),
        help="Max size of part of goban (number of stones)", metavar=('X', 'Y')
    )
    parser.add_argument(
        '--cellsize', type=float, nargs=2, default=(15, 15),
        help="Size of one cell in mm", metavar=('X', 'Y')
    )
    parser.add_argument(
        '--stone-radius', type=float, default=6.5,
        help="Radius of stone in mm"
    )
    parser.add_argument(
        '--stone-margin', type=float, default=0,
        help="Margin of stone in mm"
    )
    parser.add_argument(
        '--borders', type=float, nargs=4, default=(5, 5, 5, 5),
        help="Borders of goban in mm",
        metavar=('UP', 'RIGHT', 'BOTTOM', 'LEFT')
    )
    parser.add_argument(
        '--thickness', type=float, default=2.0,
        help="Thickness of grid in mm"
    )
    parser.add_argument(
        '--mark-thickness', type=float, default=1.5,
        help="Thickness of mark on stone in mm"
    )
    parser.add_argument(
        '--cut-thickness', type=float, default=0.2,
        help="Thickness of cut stroke in mm"
    )
    parser.add_argument(
        '--no-engrave', default=False, action='store_true',
        help='Disable engraving'
    )
    parser.add_argument(
        '--no-cut', default=False, action='store_true',
        help='Disable cutting'
    )
    parser.add_argument(
        '--no-round-corners', default=False, action='store_true',
        help='No round corners for goban'
    )
    parser.add_argument(
        '--engrave-color', type=str, default='#000000',
        help="Engrave color in web format #000000 to #ffffff"
    )
    parser.add_argument(
        '--cut-color', type=str, default='#ff0000',
        help="Cut color in web format #000000 to #ffffff"
    )
    parser.add_argument(
        '--engraved-stones', type=int, default=181,
        help="Number of engraved (black) stones"
    )
    parser.add_argument(
        '--prefix', default='',
        help='Prefix for filenames, ' +
             'create three files named prefixrecto.svg, prefixverso.svg and '+
             'prefixbase.svg'
    )
    parser.add_argument(
        '--resolution', type=int, default=100,
        help="Resolution in pixels/mm"
    )
    conf = parser.parse_args()
    goban = GobanDraw(conf)
    goban.save()
