#!/usr/bin/env python2


# Copyright 2016 Luke Phillips (lukerazor@hotmail.com)
# 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 2 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, write to the Free Software
#Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# Extension dirs
# linux:~/.config/inkscape/extensions
# windows: [Drive]:\Program Files\Inkscape\share\extensions

import inkex
import simplestyle
import copy
import math

FOLD_GAP = 5
CROP_GAP = 2
CROP_LENGTH = 3

CSNS = ""

inkex.NSS[u'cs'] = u'http://www.razorfoss.org/cardlayoutguides/'

def PrintDebug(string):
	inkex.debug( _(str(string)) )

def RoundAndDeduplicatePoints(points):
	return sorted(list(set(map(lambda x: round(x, 3), points))))

class Point():
	def __init__(self, x, y):
		self.x = x
		self.y = y

	def rotate(self, angle, origin):
		"""
		Rotate a point counterclockwise by a given angle around a given origin.

		The angle should be given in degrees.
		"""
		rads = math.radians(angle)
		newX = origin.x + math.cos(rads) * (self.x - origin.x) - math.sin(rads) * (self.y - origin.y)
		newY = origin.y + math.sin(rads) * (self.x - origin.x) + math.cos(rads) * (self.y - origin.y)

		return Point(newX, newY)

	def add(self, point):
		return Point(self.x + point.x, self.y + point.y)

	@staticmethod
	def parsePoint(pointString):
		x, y = map(lambda v: float(v), pointString.split(","))
		return Point(x, y)

	@staticmethod
	def parse(pointString, orientationString=None):
		p1 = Point.parsePoint(pointString)
		p = Point(p1.x, p1.y)

		if orientationString != None:
			po = Point.parsePoint(orientationString)
			p = p1.add(po.rotate(270, Point(0, 0)))

		return p

class LineGeneratorBase(object):
	def __init__(self, cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
		self.UnitConverterFunc = unitConverterFunc
		self.CardWidth = cardWidth
		self.CardHeight = cardHeight
		self.CardMargin = cardMargin
		self.BleedMargin = bleedMargin

		self.PageWidth = pageWidth
		self.PageHeight = pageHeight
		self.PageMargin = pageMargin

		self.ContainerWidth = -1
		self.ContainerHeight = -1

		self.GuideOffsetsWithFold = [
			0,
			self.BleedMargin, self.CardMargin, self.CardHeight - 2*self.CardMargin, self.CardMargin, self.BleedMargin,
			2*FOLD_GAP,
			self.BleedMargin, self.CardMargin, self.CardHeight - 2*self.CardMargin, self.CardMargin, self.BleedMargin
		]

		self.GuideOffsetsNoFold = [
			0,
			self.BleedMargin, self.CardMargin,
			self.CardWidth - 2*self.CardMargin,
			self.CardMargin, self.BleedMargin
		]

	def CalcPageLeftMargin(self):
		return (self.PageWidth - self.ContentWidth) / 2.0

	def CalcPageBottomMargin(self):
		return (self.PageHeight - self.ContentHeight) / 2.0

	def DrawGuide(self, xmlParent, xpos, ypos):
		posString = "{},{}".format(xpos, ypos)
		attribs = {'position': posString, 'orientation': posString}

		inkex.etree.SubElement(xmlParent, inkex.addNS('guide',"sodipodi"), attribs)

	def ConvertPoint(self, p):
		# convert point into svg approriate values, including catering for inkscapes "alternative" axis sytem ie 0, 0 is bottom left not top left
		newX = self.UnitConverterFunc("{}mm".format(p.x))
		newY = self.PageHeight - self.UnitConverterFunc("{}mm".format(p.y))

		return Point(newX, newY)

	def DrawLine(self, xmlParent, p1, p2):
		cp1 = self.ConvertPoint(p1)
		cp2 = self.ConvertPoint(p2)

		pathStr = "M {},{} {}, {}".format(cp1.x, cp1.y, cp2.x, cp2.y)
		style = {'stroke': '#000000', 'stroke-width': self.UnitConverterFunc('0.25mm'), 'fill': 'none'}
		attribs = {'style': simplestyle.formatStyle(style), 'd': pathStr}

		inkex.etree.SubElement(xmlParent, inkex.addNS('path','svg'), attribs )

	def DrawVerticleGuides(self, xmlParent, positions, gap):
		curPos = self.CalcPageLeftMargin()
		lastPos = -1
		while curPos + self.ContainerWidth <= self.PageWidth - self.PageMargin:
			for offset in positions:
				curPos += offset
				if curPos != lastPos: # don't double draw
					self.DrawGuide(xmlParent, curPos, 0)

				lastPos = curPos

			curPos += gap

	def DrawHorizontalGuides(self, xmlParent, positions, gap):
		curPos = self.CalcPageBottomMargin()
		lastPos = -1
		while curPos + self.ContainerHeight <= self.PageHeight - self.PageMargin:
			for offset in positions:
				curPos += offset
				if curPos != lastPos: # don't double draw
					self.DrawGuide(xmlParent, 0, curPos)

				lastPos = curPos

			curPos += gap

	def GenerateFoldLines(self, xmlParent):
		lines = self.GetFoldLinePositions()

		for line in lines:
			self.DrawLine(xmlParent, line[0], line[1])

	def GenerateCropMarks(self, xmlParent):
		lines = self.GetCropMarkLines()

		for line in lines:
			self.DrawLine(xmlParent, line[0], line[1])

	@staticmethod
	def CreateLineGenerator(layout, orientation, cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
		if layout == "SIMPLE":
			return SimpleGridLineGenerator(orientation, cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)

		if orientation == "HORIZONTAL":
			return LineGeneratorForHorizontalCards(cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)

		return LineGeneratorForVerticalCards(cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)

class SimpleGridLineGenerator(LineGeneratorBase):
	def __init__(self, orientation, cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
		if orientation == "HORIZONTAL":
			super(SimpleGridLineGenerator, self).__init__(cardHeight, cardWidth, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)
		else:
			super(SimpleGridLineGenerator, self).__init__(cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)

		self.ContainerWidth = self.CardWidth + 2 * bleedMargin
		self.ContainerHeight = self.CardHeight + 2 * bleedMargin

		# num across
		# num down
		self.NumContainersAcross = int((self.PageWidth - 2*self.PageMargin) // self.ContainerWidth)	# round down division
		self.NumContainersDown = int((self.PageHeight - 2*self.PageMargin) // self.ContainerHeight)	# round down division

		# content sizes
		self.ContentWidth = self.NumContainersAcross * self.ContainerWidth
		self.ContentHeight = self.NumContainersDown * self.ContainerHeight

	def GenerateGuides(self, xmlParent):
		horizontalOffsets = self.GuideOffsetsNoFold = [
			0,
			self.BleedMargin, self.CardMargin,
			self.CardWidth - 2*self.CardMargin,
			self.CardMargin, self.BleedMargin
		]

		verticalOffsets = self.GuideOffsetsNoFold = [
			0,
			self.BleedMargin, self.CardMargin,
			self.CardHeight - 2*self.CardMargin,
			self.CardMargin, self.BleedMargin
		]

		self.DrawVerticleGuides(xmlParent, horizontalOffsets, 0)
		self.DrawHorizontalGuides(xmlParent, verticalOffsets, 0)

	def GetFoldLinePositions(self):
		return [] # no fold lines in simple grid

	def GetCropMarkLines(self):
		lines = []

		leftMargin = self.CalcPageLeftMargin()
		bottomMargin = self.CalcPageBottomMargin()

		#determine all horizontal crop marks, duplicates possible
		# figure out the ypos
		horizontal_ypos = []
		for idx in range(self.NumContainersDown):
			bottomY = self.BleedMargin
			topY = bottomY + self.CardHeight
			containerOffset = bottomMargin + idx*self.ContainerHeight

			horizontal_ypos.append(containerOffset + bottomY)
			horizontal_ypos.append(containerOffset + topY)

		horizontal_ypos = RoundAndDeduplicatePoints(horizontal_ypos) # remove duplicate positions

		horizontal_xpos = [leftMargin - CROP_GAP, self.PageWidth - leftMargin + CROP_GAP + CROP_LENGTH]
		for xpos in horizontal_xpos:
			for ypos in horizontal_ypos:
				lines.append([
					Point(xpos - CROP_LENGTH, ypos),
					Point(xpos, ypos)
				])

		#determine all vertical crop marks, duplicates possible
		# figure out the xpos
		vertical_xpos = []
		for idx in range(self.NumContainersAcross):
			leftX = self.BleedMargin
			rightX = leftX + self.CardWidth
			containerOffset = leftMargin + idx*self.ContainerWidth

			vertical_xpos.append(containerOffset + leftX)
			vertical_xpos.append(containerOffset + rightX)

		vertical_xpos = RoundAndDeduplicatePoints(vertical_xpos) # remove duplicate positions

		vertical_ypos = [bottomMargin - CROP_GAP, self.PageHeight - bottomMargin + CROP_GAP + CROP_LENGTH]
		for xpos in vertical_xpos:
			for ypos in vertical_ypos:
				lines.append([
					Point(xpos, ypos),
					Point(xpos, ypos - CROP_LENGTH)
				])

		return lines
	
class LineGeneratorForVerticalCards(LineGeneratorBase):
	def __init__(self, cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
		super(LineGeneratorForVerticalCards, self).__init__(cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)

		self.ContainerWidth = cardWidth + 2*bleedMargin
		self.ContainerHeight = 2*(cardHeight + 2*bleedMargin + FOLD_GAP)

		# num across
		self.NumContainersAcross = int((self.PageWidth - 2*self.PageMargin) // self.ContainerWidth)	# round down division

		# num down
		contentHeight = lambda n: n * self.ContainerHeight + (n - 1)*(2*FOLD_GAP)
		workingHeight = self.PageHeight - 2*self.PageMargin

		self.NumContainersDown = int(workingHeight // self.ContainerHeight) # round down division for nominal value
		if contentHeight(self.NumContainersDown) > workingHeight:
			self.NumContainersDown -= 1

		# content sizes
		self.ContentWidth = self.NumContainersAcross * self.ContainerWidth
		self.ContentHeight = contentHeight(self.NumContainersDown)

	def GenerateGuides(self, xmlParent):
		self.DrawVerticleGuides(xmlParent, self.GuideOffsetsNoFold, 0)
		self.DrawHorizontalGuides(xmlParent, self.GuideOffsetsWithFold, 2*FOLD_GAP)

	def GetFoldLinePositions(self):
		lines = []
		leftMargin = self.CalcPageLeftMargin()

		for idx in range(self.NumContainersDown):
			foldY = self.CalcPageBottomMargin() + idx*(self.ContainerHeight + 2*FOLD_GAP) + self.ContainerHeight/2
			lines.append([Point(leftMargin, foldY), Point(self.PageWidth - leftMargin, foldY)])

		return lines

	def GetCropMarkLines(self):
		lines = []

		leftMargin = self.CalcPageLeftMargin()
		bottomMargin = self.CalcPageBottomMargin()
		vertical_ypos = []
		# determine all of the hornzontal facing crop marks, no duplicates possible
		for idx in range(self.NumContainersDown):
			bottomY = self.BleedMargin
			topY = bottomY + self.CardHeight
			containerOffset = bottomMargin + idx*(self.ContainerHeight + 2*FOLD_GAP)
			vertical_ypos += [
				containerOffset - CROP_GAP,
				containerOffset + 2*self.BleedMargin + 2*self.CardMargin + self.CardHeight + CROP_GAP + CROP_LENGTH
			] # stash for later

			for ypos in [containerOffset + bottomY, containerOffset + topY]:
				for xpos in [leftMargin - CROP_GAP, self.PageWidth - leftMargin + CROP_GAP + CROP_LENGTH]:
					lines.append([
						Point(xpos, ypos),
						Point(xpos - CROP_LENGTH, ypos)
					])

		#determine all vertical crop marks, duplicates possible
		# figure out the xpos
		vertical_xpos = []
		for idx in range(self.NumContainersAcross):
			leftX = self.BleedMargin
			rightX = leftX + self.CardWidth
			containerOffset = leftMargin + idx*self.ContainerWidth

			vertical_xpos.append(containerOffset + leftX)
			vertical_xpos.append(containerOffset + rightX)

		vertical_xpos = list(set(vertical_xpos)) # remove duplicate positions

		for xpos in vertical_xpos:
			for ypos in vertical_ypos:
				lines.append([
					Point(xpos, ypos),
					Point(xpos, ypos - CROP_LENGTH)
				])

		return lines

class LineGeneratorForHorizontalCards(LineGeneratorBase):
	def __init__(self, cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
		super(LineGeneratorForHorizontalCards, self).__init__(cardWidth, cardHeight, cardMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)

		self.ContainerWidth = 2*(cardHeight + 2 * bleedMargin + FOLD_GAP)
		self.ContainerHeight = cardWidth + 2 * bleedMargin

		# num across
		contentWidth = lambda n: n * self.ContainerWidth + (n - 1)*(2*FOLD_GAP)
		workingWidth = self.PageWidth - 2*self.PageMargin

		self.NumContainersAcross = int(workingWidth // self.ContainerWidth) # round down division for nominal value
		if contentWidth(self.NumContainersAcross) > workingWidth:
			self.NumContainersAcross -= 1

		# num down
		self.NumContainersDown = int((self.PageHeight - 2*self.PageMargin) // self.ContainerHeight)	# round down division

		# content sizes
		self.ContentWidth = contentWidth(self.NumContainersAcross)
		self.ContentHeight = self.NumContainersDown * self.ContainerHeight

	def GenerateGuides(self, xmlParent):
		self.DrawVerticleGuides(xmlParent, self.GuideOffsetsWithFold, 2*FOLD_GAP)
		self.DrawHorizontalGuides(xmlParent, self.GuideOffsetsNoFold, 0)

	def GetFoldLinePositions(self):
		lines = []
		bottomMargin = self.CalcPageBottomMargin()

		for idx in range(self.NumContainersAcross):
			foldX = self.CalcPageLeftMargin() + idx*(self.ContainerWidth + 2*FOLD_GAP) + self.ContainerWidth/2
			lines.append([Point(foldX, bottomMargin), Point(foldX, self.PageHeight - bottomMargin)])

		return lines

	def GetCropMarkLines(self):
		lines = []

		leftMargin = self.CalcPageLeftMargin()
		bottomMargin = self.CalcPageBottomMargin()
		horizontal_xpos = []

		# determine all of the vertical facing crop marks, no duplicates possible
		for idx in range(self.NumContainersAcross):
			leftX = self.BleedMargin
			rightX = leftX + self.CardHeight
			containerOffset = leftMargin + idx*(self.ContainerWidth + 2*FOLD_GAP)
			horizontal_xpos += [
				containerOffset - CROP_GAP,
				containerOffset + 2*self.BleedMargin + 2*self.CardMargin + self.CardHeight + CROP_GAP + CROP_LENGTH
			] # stash for later

			for xpos in [containerOffset + leftX, containerOffset + rightX]:
				for ypos in [bottomMargin - CROP_GAP, self.PageHeight - bottomMargin + CROP_GAP + CROP_LENGTH]:
					lines.append([
						Point(xpos, ypos),
						Point(xpos, ypos - CROP_LENGTH)
					])

		#determine all horizontal crop marks, duplicates possible
		# figure out the xpos
		horizontal_ypos = []
		for idx in range(self.NumContainersDown):
			bottomY = self.BleedMargin
			topY = bottomY + self.CardWidth
			containerOffset = bottomMargin + idx*self.ContainerHeight

			horizontal_ypos.append(containerOffset + bottomY)
			horizontal_ypos.append(containerOffset + topY)

		horizontal_ypos = RoundAndDeduplicatePoints(horizontal_ypos) # remove duplicate positions

		for ypos in horizontal_ypos:
			for xpos in horizontal_xpos:
				lines.append([
					Point(xpos, ypos),
					Point(xpos - CROP_LENGTH, ypos)
				])

		return lines

class FoldedCardLayoutGuidesEffect(inkex.Effect):
	def __init__(self):
		inkex.Effect.__init__(self)

		self.OptionParser.add_option('-l', '--layout', action = 'store', type = 'string', dest = 'Layout')
		self.OptionParser.add_option('-w', '--card_width', action = 'store', type = 'float', dest = 'CardWidth')
		self.OptionParser.add_option('-d', '--card_height', action = 'store', type = 'float', dest = 'CardHeight')
		self.OptionParser.add_option('-o', '--orientation', action = 'store', type = 'string', dest = 'Orientation')
		self.OptionParser.add_option('-c', '--card_margin', action = 'store', type = 'float', dest = 'CardMargin')
		self.OptionParser.add_option('-b', '--bleed_margin', action = 'store', type = 'float', dest = 'BleedMargin')
		self.OptionParser.add_option('-p', '--page_margin', action = 'store', type = 'float', dest = 'PageMargin')

	def effect(self):
		# find dimensions of page
		pageWidth = self.uutounit(self.unittouu(self.getDocumentWidth()), "mm")
		pageHeight = self.uutounit(self.unittouu(self.getDocumentHeight()), "mm")

		opt = self.options

		guideParent = self.document.xpath('//sodipodi:namedview',namespaces=inkex.NSS)[0]

		### GUIDES
		# remove all the existing guides
		[node.getparent().remove(node) for node in self.document.xpath('//sodipodi:guide',namespaces=inkex.NSS)]

		# create the generator object
		gen = LineGeneratorBase.CreateLineGenerator(opt.Layout, opt.Orientation, opt.CardWidth, opt.CardHeight, opt.CardMargin, opt.BleedMargin, pageWidth, pageHeight, opt.PageMargin, self.unittouu)

		gen.GenerateGuides(guideParent)

		### FOLD LINES
		# remove any existing 'Crop marks' layer
		[node.getparent().remove(node) for node in self.document.xpath("//svg:g[@inkscape:label='Crop Marks']",namespaces=inkex.NSS)]

		svg = self.document.xpath('//svg:svg', namespaces=inkex.NSS)[0]
		layer = inkex.etree.SubElement(svg, inkex.addNS('g',"svg"), {})
		layer.set(inkex.addNS('label', 'inkscape'), "Crop Marks")
		layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
		#layer.set(inkex.addNS('insensitive', 'sodipodi'), 'true')

		gen.GenerateFoldLines(layer)

		### CROP MARKS
		gen.GenerateCropMarks(layer)

if __name__ == '__main__':
	effect = FoldedCardLayoutGuidesEffect()

	effect.affect()
