# -*- coding: utf-8 -*-

#  Copyright © 2015-2017  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

from math import sqrt

from pybiklib.debug import DEBUG_INVISIBLE
from pybiklib.utils import epsilon
from .geom import Vector, Polyhedron, Plane, distance_axis_vert, distance_axis_edge, HalfFace, HalfCell


N_ = lambda t: t


class ModelBase:
    reversepick = False
    
    def __init__(self):
        assert all(fn.islower() for fn in self.facenames)
        assert all(' ' not in fn for fn in self.facenames)
        self.facekeys = [fn.replace('-', '_') for fn in self.facenames]
        self.cell = self.create_single_cell()
        self.axes = self.create_axes()
        self.normals = {hf.face.id: hf.normal() for hf in self.cell.halffaces}
        
    def create_axes(self):
        hf_by_id = {hf.face.id: hf for hf in self.cell.halffaces}
        return [hf_by_id[s].center().normalised() for s in self.symbols]
        
    def scale_cell(self, polys, sizes):
        saxes = (a*s for a, s in zip(self.axes, sizes))
        scale1 = (sqrt(sum(f*f for f in a)) for a in zip(*self.axes))
        scaleS = (sqrt(sum(f*f for f in a)) for a in zip(*saxes))
        scale = tuple(s/s1 for s, s1 in zip(scaleS, scale1))
        polys.scale(scale)
        
    def split_cell(self, polys, sizes):
        cuts = {}
        split_id = len(polys.faces)
        axesdata = []
        for iaxis, axis in enumerate(self.axes):
            axis = Vector(axis)
            plane = Plane(axis)
            distances = polys.distances(plane)
            axesdata.append((iaxis, axis, plane, distances))
        for (iaxis, axis, plane, distances), size in zip(axesdata, sizes):
            for plane.distance in self.splits(iaxis, size, range(1, size), distances):
                polys.split_plane(plane, iaxis, split_id)
                cuts[split_id] = axis
                split_id += 1
        return cuts
        
    @staticmethod
    def mark_invisible_faces(polys, cuts):
        for split_id, axis in cuts.items():
            visible_limit = 10000
            for cell in polys.cells:
                for hf in cell.halffaces:
                    if hf.face.type == 'face':
                        for he in hf.halfedges:
                            other_face = he.other.halfface.face
                            if other_face.type == 'cut' and other_face.id == split_id:
                                dist_ae = distance_axis_edge(axis, he.verts)
                                if dist_ae < visible_limit:
                                    visible_limit = dist_ae
            # mark all faces whose verts are within visible_limit around axis
            for face in polys.faces:
                if face.type == 'cut' and face.id == split_id:
                    for vert in face.verts:
                        dav = distance_axis_vert(axis, vert)
                        if dav > visible_limit - epsilon:
                            break
                    else:
                        face.type += '_removed'
                        
    @classmethod
    def remove_cells(self, polys, cuts):
        self.mark_invisible_faces(polys, cuts)
        if DEBUG_INVISIBLE:
            return
        polys.remove_cells(lambda c: all(f.type.endswith('_removed') for f in c.faces))
        
    def create_cells(self, sizes):
        self.cell.indices = (0,) * len(sizes)
        polys = Polyhedron()
        polys.cells = [self.cell]
        polys = polys.clone()
        self.scale_cell(polys, sizes)
        cuts = self.split_cell(polys, sizes)
        self.remove_cells(polys, cuts)
        return polys
        
        
class BrickModel (ModelBase):
    type = 'Brick'
    fileclass = 'b'
    name = N_('Brick')
    mformat = N_('{0}×{1}×{2}-Brick')
    sizenames = (N_('Width:'), N_('Height:'), N_('Depth:'))
    defaultsize = (4, 2, 3)
    
    slicesmode = 'all'
    symmetries = 2, 2, 2
    symbols = 'LDB'
    symbolsI = 'RUF'
    faces = 'UDLRFB'
    facenames = (N_('up'), N_('down'), N_('left'), N_('right'), N_('front'), N_('back'))
    default_rotation = (-30.,39.)
    
    @classmethod
    def norm_sizes(cls, size):
        y, z, x = sorted(size)
        return (x, y, z), (x, y, z)
        
    @staticmethod
    def create_single_cell():
        #              standard
        #             orientation    face symbols
        # *------*                     +---+
        # |\     |\      y             | U |
        # | *------*     |         +---+---+---+---+
        # | |    | |     o--x      | L | F | R | B |
        # *-|----* |      \        +---+---+---+---+
        #  \|     \|       z           | D |
        #   *------*                   +---+
        up = HalfFace.polygon((-1., -1.), 4, ids='FRBL')
        cell = up.extrude_y(1., -1., ids='UD')
        return cell
        
    def splits(self, iaxis, size, isplits, distances):
        return ((size - 2.*i) for i in isplits)
        
        
class TowerModel (BrickModel):
    type = 'Tower'
    name = N_('Tower')
    mformat = N_('{0}×{1}-Tower')
    sizenames = (N_('Basis:'), N_('Height:'))
    defaultsize = (2, 3)
    symmetries = 2, 4, 2
    
    @classmethod
    def norm_sizes(cls, size):
        x, y = size
        return (x, y), (x, y, x)
        
        
class CubeModel (BrickModel):
    type = 'Cube'
    name = N_('Cube')
    mformat = N_('{0}×{0}×{0}-Cube')
    sizenames = (N_('Size:'),)
    defaultsize = (3,)
    symmetries = 4, 4, 4
    
    @classmethod
    def norm_sizes(cls, size):
        x, = size
        return (x,), (x, x, x)
        
        
class VoidCubeModel (BrickModel):
    type = 'Cube3Void'
    name = N_('Void Cube')
    mformat = N_('Void Cube')
    sizenames = ()
    defaultsize = ()
    symmetries = 4, 4, 4
    
    @classmethod
    def norm_sizes(cls, unused_size):
        return (), (3, 3, 3)
        
    @classmethod
    def remove_cells(self, polys, cuts):
        polys.remove_cells(lambda c: sum(int(i==1) for i in c.indices) >= 2)
        
        
class TetrahedronModel (ModelBase):
    type = 'Tetrahedron'
    fileclass = 't'
    name = N_('Tetrahedron')
    mformat = N_('{0}-Tetrahedron')
    sizenames = (N_('Size:'),)
    defaultsize = (3,)
    
    slicesmode = 'all'
    symmetries = 3, 3, 3, 3
    symbols = 'LDBR'
    symbolsI = 'WXYZ'
    faces = 'DLRB'
    facenames = (N_('down'), N_('left'), N_('right'), N_('back'))
    default_rotation = (-10.,39.)
    
    @classmethod
    def norm_sizes(cls, size):
        x, = size
        return (x,), (x, x, x, x)
        
    _redge = sqrt(3/2)
    _itriangle = sqrt(2) / 2
    _rtriangle = sqrt(2)
    _itetrahedron = 1 / 2
    _rtetrahedron = 3 / 2
    
    _verts = [Vector((-_redge, -_itetrahedron, -_itriangle)),
              Vector(( _redge, -_itetrahedron, -_itriangle)),
              Vector(( 0.,      _rtetrahedron, 0.)),
              Vector(( 0.,     -_itetrahedron, _rtriangle))]
              
    @classmethod
    def create_single_cell(self):
        #  vertex      standard
        #  indices    orientation    face symbols
        # 2
        # |              y
        # |              |             +-----+
        # |              o--x        /L|B\ R/
        # 0------1        \        +---+---+
        #  \               z           |D/
        #   3                          +
        down = HalfFace.polygon((0., -self._rtriangle), 3, ids='LBR')
        cell = down.pyramid(1.5, -.5, id='D')
        return cell
        
    def splits(self, iaxis, size, isplits, distances):
        low, *unused, high = distances
        return ((low + 2*i) for i in isplits)
        
        
class TriangularPrism (ModelBase):
    type = 'Prism3'
    fileclass = 't'
    name = N_('Triangular Prism')
    mformat = N_('{0}×{1} Triangular Prism')
    sizenames = (N_('Basis:'), N_('Height:'))
    defaultsize = (3, 2)
    
    slicesmode = 'all'
    symmetries = 2, 3, 2, 2
    symbols = 'LDBR'
    symbolsI = 'XUYZ'
    faces = 'UDLRB'
    facenames = (N_('up'), N_('down'), N_('left'), N_('right'), N_('back'))
    
    default_rotation = (-4.,39.)
    
    @classmethod
    def norm_sizes(cls, size):
        b, h = size
        if b > 6 or h > 6:
            return None, None
        return (b, h), (b, h, b, b)
        
    @classmethod
    def create_single_cell(self):
        #              standard      polygon
        #             orientation  orientation
        # *------*
        # |\    /|       y          y
        # |  *   |       |          |
        # |  |   |       o--x       |
        # *--|---*        \         o-----x
        #  \ |  /          z
        #    *
        _rtriangle = sqrt(2)
        up = HalfFace.polygon((0., -_rtriangle), 3, ids='RBL')
        cell = up.extrude_y(1., -1., ids='UD')
        return cell
        
    split_arg = 0
    
    def splits(self, iaxis, size, isplits, distances):
        low, high = distances
        if iaxis == 1:
            return ((low + 2*i) for i in isplits)
        else:
            d = (high - low) / (size+self.split_arg)
            return ((low + i*d) for i in isplits)
            
    
class TriangularPrismComplex (TriangularPrism):
    type = 'Prism3Complex'
    fileclass = 't'
    name = N_('Triangular Prism (complex)')
    mformat = N_('{0}×{1} Triangular Prism (complex)')
    sizenames = (N_('Basis:'), N_('Height:'))
    defaultsize = (3, 2)
    
    split_arg = -1/3
    
    
class PentagonalPrism (ModelBase):
    type = 'Prism5'
    fileclass = 'p'
    name = N_('Pentagonal Prism')
    mformat = N_('{0}×{1} Pentagonal Prism')
    sizenames = (N_('Basis:'), N_('Height:'))
    defaultsize = (2, 2)
    
    slicesmode = 'last1'
    symmetries = 2, 5, 2, 2, 2, 2
    symbols =  'AUBCDE'
    symbolsI = 'GLHIJK'
    faces = 'ULABCDE'
    facenames = (N_('up'), N_('down'),
                 N_('front-right'), N_('back-right'), N_('back'), N_('back-left'), N_('front-left'))
    
    default_rotation = (-4.,39.)
    
    @classmethod
    def norm_sizes(cls, size):
        b, h = size
        if b > 6 or h > 6:
            return None, None
        return (b, h), (b, h, b, b, b, b)
        
    def create_single_cell(self):
        up = HalfFace.polygon(Vector((0, -1.4)), 5, ids='ABCDE')
        cell = up.extrude_y(1., -1., ids='UL')
        return cell
        
    side_center = 0
        
    def splits(self, iaxis, size, isplits, distances):
        if iaxis == 1:
            low, unused = distances
            return ((low + 2*i) for i in isplits)
        elif size == 1:
            return ()
        else:
            unused, mid, high = distances
            d = (high - mid) / (size-1+self.side_center) / 2
            low = high - size*d
            return (i*d + low for i in isplits)
            
        
class PentagonalPrismM (PentagonalPrism):
    type = 'Prism5M'
    fileclass = 'p'
    name = N_('Pentagonal Prism (stretched)')
    mformat = N_('{0}×{1} Pentagonal Prism (stretched)')
    
    side_center = 0.2
            
        
class Octahedron (ModelBase):
    type = 'Octahedron'
    fileclass = 't'
    name = N_('Octahedron')
    mformat = N_('{0}-Octahedron')
    sizenames = (N_('Size:'),)
    defaultsize = (3,)
    
    slicesmode = 'all'
    symmetries = 3, 3, 3, 3
    symbols = 'ELCA'
    symbolsI = 'BUFD'
    faces = 'LEACUBDF'
    facenames = (N_('down'), N_('front-left'), N_('front-right'), N_('back'),
                 N_('up'), N_('back-right'), N_('back-left'), N_('front'))
    default_rotation = (-10.,39.)
    
    reversepick = True
    
    @classmethod
    def norm_sizes(cls, size):
        x, = size
        if x > 8:
            return None, None
        return (x,), (x, x, x, x)
        
    @classmethod
    def create_single_cell(self):
        tetra_to_octa = dict(('DL','RA','BC','LE'))
        cid_to_faceid = {'AEL':'F', 'ACL':'B', 'ACE':'U', 'CEL':'D'}
        polys = TetrahedronModel().create_cells((2, 2, 2, 2))
        for f in polys.faces:
            if f.type == 'face':
                f.id = tetra_to_octa[f.id]
        for c in polys.cells:
            cid = ''
            face = None
            for f in c.faces:
                if f.type == 'face':
                    cid += f.id
                elif face is None:
                    face = f
                else:
                    face = False
            cid = ''.join(sorted(cid))
            if face:
                face.type = 'face'
                face.id = cid_to_faceid[cid]
        polys.remove_cells(lambda c: sum(c.indices) != 0)
        assert len(polys.cells) == 1
        return polys.cells[0]
        
    def splits(self, iaxis, size, isplits, distances):
        low, *unused, high = distances
        return ((low + 2*i) for i in isplits)
        
        
class Dodecahedron (ModelBase):
    type = 'Dodecahedron'
    fileclass = 'd'
    name = N_('Dodecahedron')
    mformat = N_('{0}-Dodecahedron')
    sizenames = (N_('Size:'),)
    defaultsize = (3,)
    
    slicesmode = 'mid'
    symmetries = 5, 5, 5, 5, 5, 5
    symbols =  'ABCDEF'
    symbolsI = 'MKLGHI'
    faces = 'ABCDEFGHIKLM'
    facenames = (N_('down'), N_('down-back-right'), N_('down-front-right'), 
                 N_('down-front'), N_('down-front-left'), N_('down-back-left'),
                 N_('up-back'), N_('up-back-right'), N_('up-front-right'),
                 N_('up-front-left'), N_('up-back-left'), N_('up'))
    default_rotation = (-10.,39.)
    
    reversepick = False
    
    @classmethod
    def norm_sizes(cls, size):
        x, = size
        if x%2 != 1:
            return None, None
        return (x,), (x, x, x, x, x, x)
        
    @classmethod
    def create_single_cell(self):
        cell = HalfCell.dodecahedron(ids=self.faces)
        return cell
        
    def splits(self, iaxis, size, isplits, distances):
        low, first, *unused, high = distances
        slicewidth = (first-low)/(size-.5)
        def ifunc(i):
            if i*2 < size:
                r = low + i*slicewidth
            else:
                r = high + (i - size)*slicewidth
            return r
        return (ifunc(i) for i in isplits)
        
        
        
modeldefs = [CubeModel, TowerModel, BrickModel,
             TetrahedronModel, Octahedron, Dodecahedron,
             VoidCubeModel,
             TriangularPrism, TriangularPrismComplex,
             PentagonalPrism, PentagonalPrismM,
             ]
             

