#!/usr/bin/env python3
#

# Walk through the color spectrum in an orderly fashion and display the colors in Inkscape,

	# Language: Simple Inkscape Scripting (SIS, apparently)
# Details: https://inkscape.org/~pakin/%E2%98%85simple-inkscape-scripting
# Github: https://github.com/spakin/SimpInkScr

# Developed against Python 3.8.10

# rgbx version 1.0
#
# Copyright 2022 Joseph Smith
#
# Permission to use, copy, modify, and/or distribute this software for any purpose with or without
# fee is hereby granted, provided that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.

# Changes:
# - exceedpageborders -> spillover
# - objectstroke -> objectstrokecolor
# - dropped support for buggy page dimensions handling (pagewidth, pageheight)
# - simplified and improved skip/shuffle/printvalues handling (separate functions)

# TODO: maybe: fieldalign (left/center/right), for multiple runs

# Bugs/shortcomings
# - turnaround only works with plainrgb color set, which suggests it doesn't survive a recharge
# - no repetitive output with colorfile
# - FIXED: fieldkip plus shufflecolors will shuffle skipped fields as well
# - FIXED: fieldskip plus invertcolors: TypeError: unsupported operand type(s) for -: 'int' and 'str'
# - FIXED: controlling the script from a config file is cumbersome (added GUI)

# --------------------------------------------------------------------------------------------------
# Q and A secion. 
#
# Q: why not group functions neatly into objects?
# A: because doing so would bring lots of overhead with little or no real gain.
#
# Q: why so many global variables?
# A: because having them snake through the code in the form of function parameters would result in
#    a maintenance nightmare (tried and failed).
#
# Having said that, the initialization section must rank high among the ugliest code blocks ever
# written, but no amount of de-globalization or objectification would fix that.
# --------------------------------------------------------------------------------------------------


import os, sys
import os.path

# Config file written by RGBX-GUI
CONFIGFILE = 'rgbx-config.txt'


# ==================================================================================================

# Function definitions


# CONFIG

# Parse configuration file; sets globals directly
def loadconfig(cffile):

  # For maintainers to maintain: expected types CAN be speciified; without, everything returns as str.
  # Lists of (quoted) config var names.
  expectint = ['backgroundcolor', 'colorstep', 'fieldnum', 'fieldheight', 'fieldsep', 'fieldskip', 'skipoffset', 'turnaround', 'huerotation', 'objectsize', 'minobjectsize', 'objectstrokewidth', 'rotatecolor']
  expectfloat = ['fieldopacity', 'objectopacity']

  # List of config params read from the file.
  cflist = []

  # Read in file contents.
  with open(cffile) as fp:
    lines = fp.readlines()
    for line in lines:
      if '#' in line:
        parts = line.split('#')
        line = parts[0].rstrip()
      else:
        line = line.rstrip()
      if '=' in line:
        entry = line.split('=')
        cfname = entry[0].strip()
        cfval = entry[1].strip()
        cfval = cfval.replace("'", "")
        cfval = cfval.replace('"', "")
        configentry = [cfname, cfval]
        cflist.append(configentry)
  fp.close()

  # Set global variables.
  for i in range(len(cflist)):
    name = cflist[i][0]
    value = cflist[i][1]
    if (value.startswith("0x")):
      iv = int(value[2:], 16)
      globals()[name] = int(iv)
    elif name in expectint:
      iv = int('%s' % value, 0)
      globals()[name] = iv
    elif name in expectfloat:
      fv = float('%s' % value)
      globals()[name] = float(fv)
    else:
      globals()[name] = value

  return()


# ===============================================================================

# HIGH-LEVEL FUNCTIONS

# - all of these are called from the main code
# - most of these prepare the final print list


# Create a list of fields to be printed or skipped.
# This is in fact the MAIN LOOP - the highest-level function in this script.
def createprintlist():
  global RGBCODE, CHANGEOP, fieldcount
  for i in range(fieldnum):
    # Update the print list, keeping field skips into account
    PRINTLIST.append(RGBCODE)
    # Change values of color bytes for next rum
    RGBCODE = updatergb(RGBCODE)
    # Switch roles of color bytes after every N runs
    if ((i + 1) % turnaround == 0):
      # R-B-G to R-G-B output: reverse shift
      # Works only with an M/R index swap at initialization; see initlmr()
      if (swapgb == 'yes'): shiftlmr('right')
      else: shiftlmr('left')
      if (colorset == 'web'):
        # No need to recharge bytes
        # The shift has taken care of the correct index switch (M becomes former L/M)
        if (CHANGEOP == 'inc'): CHANGEOP = 'dec'
        elif (CHANGEOP == 'dec'): CHANGEOP = 'inc'
      elif (colorset != 'plainrgb'):
        # Recharge bytes (set values high)
        RGBCODE = rechargergb(RGBCODE)


# Reshuffle print list.
def handleshuffle():
  if (shufflecolors == 'yes'):
    from random import random, shuffle
    shuffle(PRINTLIST)


# Invert colors.
def handleinvert():
  if (invertcolors == 'yes'):
    for i in range(len(PRINTLIST)):
      PRINTLIST[i] = 0xffffff - PRINTLIST[i]


# For handleskips(): decide whether to skip a field.
# Assumes (should only be calles with) a fieldskip value > 0.
def testskip(fnum):

  skip = 0

  if (fnum < skipoffset):
    skip = 1
  # Complex condition, admittedly rooted in trial, error and heuristics.
  elif ((fnum - skipoffset) % (fieldskip + 1) != 0):
    skip = 1

  if (invertskip == 'yes'):
    skip = 1 - skip

  return(skip)


# Handle field skip.
def handleskips():
  global PRINTLIST, skipcount
  # No need to traverse the entire print list once more if fieldskip equals 0.
  if (fieldskip > 0):
    for i in range(fieldnum):
      if testskip(i):
        PRINTLIST[i] = 'skip'


# Print color values.
def handleprintvalues():
  if (printvalues == 'yes'):
    for i in range(len(PRINTLIST)):
      if (PRINTLIST[i] != 'skip'):
        print('%d:	%06x' % (i+1, PRINTLIST[i]))


# Read 0x-less hex rgb codes from a file to be used as print list.
def readcolorfile(rgbf):
  rgbl = []  
  with open(rgbf, 'r') as fp:
    line = fp.readline()
    while line:
      if '#' in line:
        parts = line.split('#')
        line = parts[0]
      line = line.strip()
      if line:
        rgbl.append(str2hex(line))
      line = fp.readline()
  fp.close()
  return(rgbl)


# ===============================================================================

# COLOR SET PROCESSING


# Change RGB byte values for next rum (called from main loop)
def updatergb(rgbval):
  if (colorset == 'darkrgb'):
    # RGB colors: decrement R bytes only
    rgbval = changecolorbytes(rgbval, rgblist[lindex], 'ignore', 'ignore', -1, 0, 0, colorstep)
  elif (colorset == 'plainrgb'):
    # Plainrgb colors: increment L bytes, decrement R bytes; the W bytes remain 00 throughout
    rgbval = changecolorbytes(rgbval, rgblist[lindex], 'ignore', rgblist[rindex], -1, 0, 1, colorstep)
  elif (colorset == 'hybrid'):
    # Hybrid colors: increment L bytes, decrement W and R bytes
    rgbval = changecolorbytes(rgbval, rgblist[lindex], rgblist[mindex], rgblist[rindex], -1, -1, 1, colorstep)
  elif (colorset == 'lightrgb'):
    # Bright colors: decrement W bytes only
    rgbval = changecolorbytes(rgbval, 'ignore', rgblist[mindex], 'ignore', 0, -1, 0, colorstep)
  elif (colorset == 'darkmyc'):
    # MYC colors: decrement W and R bytes
    rgbval = changecolorbytes(rgbval, rgblist[lindex], 'ignore', rgblist[mindex], -1, 0, -1, colorstep)
  elif (colorset == 'web'):
    # 'Web safe' colors: decrement M __OR__ increment R bytes, depending on direction
    if (CHANGEOP == 'inc'):
      rgbval = changecolorbytes(rgbval, 'ignore', rgblist[mindex], 'ignore', 0, 1, 0, colorstep)
    elif (CHANGEOP == 'dec'):
      rgbval = changecolorbytes(rgbval, 'ignore', rgblist[mindex], 'ignore', 0, -1, 0, colorstep)
  return(rgbval)


# Recharge bytes (set them to 0xff or ceiling value)
def rechargergb(rgbval):
  if (colorset == 'hybrid'):
    rgbval = chargecolorbytes(rgbval, rgblist[mindex])
  elif (colorset == 'darkmyc'):
    rgbval = chargecolorbytes(rgbval, rgblist[mindex])
    rgbval = chargecolorbytes(rgbval, rgblist[lindex])
  elif (colorset == 'lightrgb'):
    rgbval = chargecolorbytes(rgbval, rgblist[lindex])
  elif (colorset == 'darkrgb'):
### Could handle turnaround here
    rgbval = chargecolorbytes(rgbval, rgblist[lindex])
  return(rgbval)


# -------------------------------------------------------------------------------

# LOW LEVEL RGB MANAGEMENT


# Reset color values (vulnerable to overflow)
def chargecolorbytes(clr, clname):
  curvalue =  getbyteval(clr, clname)
  if (clname == 'r'):
    ceiling = CEILINGR
    #threshold = THRESHOLDR
  elif (clname == 'g'):
    ceiling = CEILINGG
    #threshold = THRESHOLDG
  elif (clname == 'b'):
    ceiling = CEILINGB
    #threshold = THRESHOLDB
  else:
    return(-1)
  newvalue = ceiling - curvalue
  newclr = setbyteval(clr, newvalue, clname)
  return(newclr)


# Change the rgb value
# The order of names doesn't really matter, as long as signs correspond
# If a name is tagged 'ignore', the value of the sign argument is irrelevant
def changecolorbytes(clr, lname, wname, rname, lsign, wsign, rsign, inc):
  newrgbv = clr
  cnames = (lname, wname, rname)	# values: 'r', 'g', 'b', 'ignore'
  csigns = (lsign, wsign, rsign)	# values: -1, 1
  for i in range(len(cnames)):
    if (cnames[i] != 'ignore'):
      newrgbv = setbyteval(newrgbv, csigns[i] * inc, cnames[i])
  return(newrgbv)


# Change the value of a byte for specified color (vulnerable to overflow)
def setbyteval(clr, clbytes, clname):
  if (clname == 'r'):
    newclr = clr + clbytes * 0x10000
  elif (clname == 'g'):
    newclr = clr + clbytes * 0x100
  elif (clname == 'b'):
    newclr = clr + clbytes
  else:
    return(-1)
  return(newclr)


# Retrieve the value of a byte for specified color
def getbyteval(clr, clname):
  if (clname == 'r'):
    # The modulo operation flushes out any overflow
    clbytes = int(clr / 0x10000) % 0x100
  elif (clname == 'g'):
    clbytes = int(clr / 0x100) % 0x100
  elif (clname == 'b'):
    clbytes = int(clr) % 0x100
  else:
    return(-1)
  return(clbytes)


# -------------------------------------------------------------------------------

# INDEX/SWITCH HANDLING FOR MAIN LOOP


# Set initial indices of RGB bytes in rgblist: 0, 1, 2; with red startcolor: R, G, B
# This is the only occasion where LMR meets RGB concretely, and only through the L index
# R and M values (positions in RGB list) are relative to that of L
# The heart of rotational complexity beats here; see the README about LMR vs LRM
def initlmr(rgboffs):
  global rindex, mindex, lindex
  lindex = rgboffs				# L: offset in {r, g, b} list
  if (swapgb == 'yes'):			# This ALSO requires a shift R i.o. a shift L
    mindex = (lindex + 2) % 3			# M = L + 2	}
    rindex = (mindex + 2) % 3			# R = L + 1	} => LRM (swapped scheme RBG)
  else:
    rindex = (lindex + 2) % 3			# R = L + 2	}
    mindex = (rindex + 2) % 3			# M = L + 1	} => LMR (plainrgb scheme RGB)


# Handle R/L index shifts
# Shift values: 'left', 'right'; anything other means no shift is performed
def shiftlmr(shift):
  global rindex, mindex, lindex
  if (shift == 'left'):			# used with LMR (plainrgb scheme RGB)
    # new X = old X - 1: left shift
    rindex = (rindex + 2) % 3
    mindex = (mindex + 2) % 3
    lindex = (lindex + 2) % 3
  if (shift == 'right'):			# NEEDED with LRM (swapped scheme RBG)
    # new X = old X + 1: right shift
    rindex = (rindex + 4) % 3
    mindex = (mindex + 4) % 3
    lindex = (lindex + 4) % 3


# -------------------------------------------------------------------------------

# FIELD PRINTING


# Print items from the rgb list taking field skips into account
# This one should be easy to replace with various other routines
# E.g. print a bunch of circles (or Super Marios) using the colors from the list 
def printfields(plist):
  global ymin
  for i in range(len(plist)):
    if (plist[i] != 'skip'):
      if (type(plist[i]) == int):
        value = plist[i]
      else:
        value = int(plist[i], 0)
      fld = fieldprep(xmin, ymin, xmin + fieldwidth, ymin + fieldheight, value)
      l1.add(fld)
    # The '- 1' should largely do away with annoying 1px-wide stipes
    ymin = ymin + fieldheight + fieldsep - stripefix


# Examine field printing conditions; handle color inversion
def fieldprep(x0, y0, x1, y1, fcolor):
  # Wrap color if its value exceeds the 000000-ffffff range. Best effort / quick fix.
  # Could be handled per byte instead, with equally little meaning
  if (wrapcolors == 'yes'):
    if (fcolor < 0):
      fcolor = -fcolor
    while (fcolor > 0xffffff):
      fcolor = int(fcolor / 0x10)
  # Print first, adjust indices later
  fld = writefield(x0, y0, x1, y1, fcolor)
  return(fld)


# Function: display a color field
def writefield(x0, y0, x1, y1, clr):
  fieldcolor = hex2str(clr)
  f = rect((x0, y0), (x1, y1), stroke=0, stroke_width=0, opacity='%f' % fieldopacity, fill = '#%s' % fieldcolor)
  return(f)


def writebackground(x0, y0, x1, y1, clr):
  fieldcolor = hex2str(clr)
  f = rect((x0, y0), (x1, y1), stroke=0, stroke_width=0, fill = '#%s' % fieldcolor)
  return(f)


# -------------------------------------------------------------------------------

# LOW LEVEL


# Turn a numeric color value into a 6-digit (padded) hex string
def hex2str(value, chars = 6):
  return('{0:0{1}x}'.format(value, chars))


# Turn a hex color string into a numeric value
# Unused, but has won its spurs for debugging
def str2hex(string):
  rv = int('0x%s' % string, 0)
  return(rv)


def restrict(v, minval, maxval):
  if v < minval: return minval
  if v > maxval: return maxval
  return(v)


# ===============================================================================

# BONUS CODE: print objects i.o printfields

# TODO: provide a better hook mechanism for insertable code.


# Return a random object position
def getobjectpos(pwidth, pheight, size):
  if (spillover == 'yes'):
    (x, y) = uniform(0, pwidth), uniform(0, pheight)
  else:
    (x, y) = uniform(size, pwidth - size), uniform(size, pheight - size)
  return(x, y)


# Print randomly positioned squares using colors generated by this script
def printsquares(plist):
  for i in range(len(plist)):
    if (plist[i] != 'skip'):
      (x, y) = getobjectpos(pagewidth, pageheight, objectsize)
      if (minobjectsize != 0):
        xsize = uniform(int(minobjectsize), int(objectsize))
        size = (xsize, xsize)
      else:
        size = (float(objectsize), float(objectsize))
      sqr = rect((0, 0), size, transform='translate(%g, %g)' % (x, y), stroke='%s' % objectstrokecolor, stroke_width='%d' % objectstrokewidth, opacity='%f' % float(objectopacity), fill='#%06x' % plist[i])
      l1.add(sqr)


# Print randomly positioned rectangles using colors generated by this script
# TODO: with an objectsize only things get rather vague (maybe add rectangle ratio to the config)
def printrectangles(plist):
  for i in range(len(plist)):
    if (plist[i] != 'skip'):
      (x, y) = getobjectpos(pagewidth, pageheight, objectsize)
      if (minobjectsize != 0):
        xsize = uniform(int(minobjectsize), int(objectsize))
        ysize = uniform(int(minobjectsize), int(objectsize))
        size = (xsize, ysize)
      else:
        # This is a bit random
        xsize = uniform(0, int(objectsize) * 2)
        ysize = uniform(0, int(objectsize) * 2)
        size = (xsize, ysize)
      rct = rect((0, 0), size, transform='translate(%g, %g)' % (x, y), stroke='%s' % objectstrokecolor, stroke_width='%d' % objectstrokewidth, opacity='%f' % float(objectopacity), fill='#%06x' % plist[i])
      l1.add(rct)


# Print randomly positioned circles using colors generated by this script
def printcircles(plist):
  for i in range(len(plist)):
    if (plist[i] != 'skip'):
      (x, y) = getobjectpos(pagewidth, pageheight, objectsize)
      if (minobjectsize != 0):
        size = uniform(int(minobjectsize), int(objectsize))
      else:
        size = float(objectsize) 
      # Ovals: ckl = circle((x, y), 50, transform='translate(%g, %g) scale(0.75, 1) rotate(45)' % (x, y), fill='#%06x' % PRINTLIST[i])
      ckl = circle((0, 0), size, transform='translate(%g, %g)' % (x, y), stroke='%s' % objectstrokecolor, stroke_width='%d' % objectstrokewidth, opacity='%f' % float(objectopacity), fill='#%06x' % plist[i])
      l1.add(ckl)


# Print randomly positioned triangles in randomized perspective using colors generated by this script
def printtriangles(plist):
  for i in range(len(plist)):
    if (plist[i] != 'skip'):
      (x, y) = getobjectpos(pagewidth, pageheight, objectsize)
      if (minobjectsize != 0):
        size = uniform(int(minobjectsize), int(objectsize))
      else:
        size = float(objectsize)
      trg = regular_polygon(3, (x, y), size, 2, 0.0, 0.7, stroke='%s' % objectstrokecolor, stroke_width='%d' % objectstrokewidth, opacity='%f' % float(objectopacity), fill='#%06x' % plist[i])
      l1.add(trg)


# ===============================================================================

# COLOR SET 'ROTATE'

# For color set 'rotate': rotate the hue by a specified amount of degrees.
# Code credit: Mark Ransom on Stack Overflow; adapted for specific use
# Original code: https://stackoverflow.com/questions/8507885/shift-hue-of-an-rgb-color

from math import sqrt,cos,sin,radians

rgbmatrix = [[1,0,0],[0,1,0],[0,0,1]]


def clamp(v):
  if v < 0x00: return 0x00
  if v > 0xff: return 0xff
  return int(v + 0.5)


def rotatehue(degrees):
  cosA = cos(radians(degrees))
  sinA = sin(radians(degrees))
  rgbmatrix[0][0] = cosA + (1.0 - cosA) / 3.0
  rgbmatrix[0][1] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
  rgbmatrix[0][2] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
  rgbmatrix[1][0] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
  rgbmatrix[1][1] = cosA + 1./3.*(1.0 - cosA)
  rgbmatrix[1][2] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
  rgbmatrix[2][0] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
  rgbmatrix[2][1] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
  rgbmatrix[2][2] = cosA + 1./3. * (1.0 - cosA)


def getrotatedvalue(rgbval):
  r = getbyteval(rgbval, 'r')
  g = getbyteval(rgbval, 'g')
  b = getbyteval(rgbval, 'b')
  rx = r * rgbmatrix[0][0] + g * rgbmatrix[0][1] + b * rgbmatrix[0][2]
  gx = r * rgbmatrix[1][0] + g * rgbmatrix[1][1] + b * rgbmatrix[1][2]
  bx = r * rgbmatrix[2][0] + g * rgbmatrix[2][1] + b * rgbmatrix[2][2]
  rx = clamp(rx)
  gx = clamp(gx)
  bx = clamp(bx)
  rv = 0x10000 * rx + 0x100 * gx + bx
  return(rv)


def getnexthue(rgbv, hrot):
  rotatehue(hrot)
  newrgb = getrotatedvalue(rgbv)
  if (printvalues == 'yes'): print('%06x' % RGBCODE)
  return(newrgb)


# Populate the print list for color set 'rotate'.
def createrotateprintlist():

  global RGBCODE, PRINTLIST

  testhr = int(huerotation % 360)

  if (testhr == 0):
    PRINTLIST = []
  else:
    # Initial RGB code (color to start with) was set during initialization.
    # If rotatecolor is set, use this color instead.
    if (rotatecolor != -1):
      RGBCODE = rotatecolor
    PRINTLIST = [RGBCODE]
    if (autofieldnum == 'yes'):
      rotatesteps = int(360 / huerotation)
    else:
      rotatesteps = fieldnum
    for step in range(rotatesteps - 1):
      RGBCODE = getnexthue(RGBCODE, huerotation)
      PRINTLIST.append(RGBCODE)

  return()


#====================================================================================================

# MAIN CODE: INITIALIZATION


# Load the contents of the configuration file.
loadconfig(CONFIGFILE)

# Configuration defaults.
if not 'fieldnum' in globals(): fieldnum = 0
if not 'fieldheight' in globals(): fieldheight = 0
if not 'colorstep' in globals(): colorstep = 0x33
if not 'fixstriping' in globals(): fixstriping = 'no'

if not 'fieldsep' in globals(): fieldsep = 0
if not 'fieldskip' in globals(): fieldskip = 0
if not 'skipoffset' in globals(): skipoffset = 0
if not 'invertskip' in globals(): invertskip = 'no'
if not 'pagemargin' in globals(): pagemargin = 0

if not 'showbackground' in globals(): showbackground = 'no'
if not 'backgroundcolor' in globals(): backgroundcolor = 0xffffff

if not 'colorset' in globals(): colorset = 'plainrgb'
if not 'startcolor' in globals(): startcolor = 'red'
if not 'invertcolors' in globals(): invertcolors = 'no'
if not 'fieldopacity' in globals(): fieldopacity = 1
if not 'huerotation' in globals(): huerotation = 30
if not 'rotatecolor' in globals(): rotatecolor = -1
if not 'rbgoutput' in globals(): rbgoutput = 'no'
if not 'turnaround' in globals(): turnaround = int(0xff / colorstep)
if not 'colorfile' in globals(): colorfile = ''
if not 'shufflecolors' in globals(): shufflecolors = 'no'
if not 'printvalues' in globals(): printvalues = 'no'

#----------------------------------------------------------------------------------------------------
# For BONUS CODE: print objects i.o. fields
if not 'showmesquares' in globals(): showmesquares = 'no'
if not 'showmerectangles' in globals(): showmerectangles = 'no'
if not 'showmecircles' in globals(): showmecircles = 'no'
if not 'showmetriangles' in globals(): showmetriangles = 'no'
if not 'objectsize' in globals(): objectsize = 10
if not 'minobjectsize' in globals(): minobjectsize = 0
if not 'objectstrokecolor' in globals(): objectstrokecolor = 'white'
if not 'objectstrokewidth' in globals(): objectstrokewidth = 0
if not 'objectopacity' in globals(): objectopacity = 1
if not 'spillover' in globals(): spillover = 'no'
#----------------------------------------------------------------------------------------------------

# Shelved settings (dysfunctional)
###rgbthreshold = 0x333333		# DEFAULT is 0x000000; value at which to declare underflow
###rgbceiling = 0xcccccc		# DEFAULT is 0xffffff; value at which to declare SWITCHTIME
###wrapcolors = 'yes'			# DEFAULT is 'no'; affects the entire RGB triplet
if not 'rgbthreshold' in globals(): rgbthreshold = 0x0
if not 'rgbceiling' in globals(): rgbceiling = 0xffffff
if not 'wrapcolors' in globals(): wrapcolors = 'no'

# What follows are further initializations  based on the configuration settings. While these could be
# neatly packed in descriptive functions, since we're dealing with globals that would only complicate
# matters.

#----------------------------------------------------------------------------------------------------

# (Re)initialze configuration variables depending on others. Set some control variables.

# Threshold and ceiling for byte values, normally 0x00 and 0xff respectively.
# rgbthreshold and rgbceiling are currently not in the configuration settings.
THRESHOLDR = int(rgbthreshold / 0x10000) % 0x100
THRESHOLDG = int(rgbthreshold / 0x100) % 0x100
THRESHOLDB = rgbthreshold % 0x100
CEILINGR = int(rgbceiling / 0x10000) % 0x100
CEILINGG = int(rgbceiling / 0x100) % 0x100
CEILINGB = rgbceiling % 0x100

# These allow fieldnum and fieldheight to be reused.
autofieldnum = 'no'
autofieldheight = 'no'
if (fieldnum == 0): autofieldnum = 'yes'
if (fieldheight == 0): autofieldheight = 'yes'

# Stripe fixing: shift field start up one pixel.
stripefix = 0
if (fixstriping == 'yes'): stripefix = 1

# invertskip has no meaning without fieldskip; may result in no output at all.
if (fieldskip == 0): invertskip = 0

# A turnaround value higher than the natural one makes no sense.
if (turnaround > int(0xff / colorstep)): turnaround = int(0xff / colorstep)

# Get current page dimensions from SIS
pagewidth = float(svg_root.get('width').rstrip('px'))
pageheight = float(svg_root.get('height').rstrip('px'))

# Field width equalss pagewidth, unless a margin is set.
pagemargin = float(pagemargin)
fieldwidth = pagewidth - 2 * pagemargin
# Xmin and Ymin can be easily determined; Xmax/Ymax depend on fieldheight, set later.
if (pagemargin == 0): (xmin, ymin) = (0, 0)
else: (xmin, ymin) = (pagemargin, pagemargin)

# Hex color code of current color to be printed; referred to throughout this script.
RGBCODE = None

# List of hex values for colors to be displayed.
PRINTLIST = []

# For 'web' color set
CHANGEOP = 'inc'

# Make all color sets print R-G-B by default (only 'web' does that by nature).
# This is much easier and safer than tinkering with the near-inscrutable innards of the script.
swapgb = 'no'
if (rbgoutput == 'yes'):
  if (colorset == 'web'):
    swapgb = 'yes'
elif (colorset != 'web'):
  swapgb = 'yes'

# Set uo the list of RGB color naes to rotate through.
# Sets the byte to be pointed to by the L initial index high.
rgblist = ('r', 'g', 'b')
RGBCODE = 0x000000
if (startcolor == 'red'):
  RGBCODE = chargecolorbytes(RGBCODE, 'r')
  rgboffset = 0
elif (startcolor == 'green'):
  RGBCODE = chargecolorbytes(RGBCODE, 'g')
  rgboffset = 1
elif (startcolor == 'blue'):
  RGBCODE = chargecolorbytes(RGBCODE, 'b')
  rgboffset = 2

# This hooks the initial L index onto a color name in the list. The other indices (M/R) are set
# relative to the L index. Each index will point to a different RGB color name, which is used for
# retrieving the corresponding part of the RGB value, processed (changed) over and over again.
initlmr(rgboffset)

# Initialize (set high) the M bytes (not with 'plainrgb' or 'darkrgb').
if (colorset == 'hybrid' or colorset == 'lightrgb' or colorset == 'darkmyc'):
  RGBCODE = chargecolorbytes(RGBCODE, rgblist[mindex])

# VAGUE but functional: set the R byte moderately high so that the colors repeat.
# Has no menaing for 'darkrgb' (because it wouldn't render primary colors).
if (colorset == 'hybrid' or colorset == 'lightrgb' or colorset == 'darkmyc'):
  # Vulnerable to underflow: depends on turnaround and colorstep values.
  highrbytes = 0xff - turnaround * colorstep
  # Failsafe
  if (highrbytes < 0): highrbytes = 0
  RGBCODE = RGBCODE + highrbytes

COLORFILELIST = []
# Automatic field number calculation.
if (autofieldnum == 'yes'):
  if (colorfile != ''):
    COLORFILELIST = readcolorfile(colorfile)
    fieldnum = len(COLORFILELIST)
  elif (colorset == 'rotate'):
    fieldnum = int(360 / huerotation)
  elif (colorset == 'web'):
    # Color set 'web' requires twice as many fields as the others to complete a round.
    fieldnum = int(CEILINGR / int(colorstep)) * 6
  else:
    fieldnum = int(CEILINGR / int(colorstep)) * 3

# Automatic field alignment.
if (autofieldheight == 'yes'):
  emptypagespace = 2 * pagemargin + (fieldnum - 1) * (fieldsep - stripefix)
  fieldheight = (pageheight - emptypagespace) / fieldnum


#====================================================================================================

# MAIN CODE: ACTION


# POPULATE PRINT THE LIST

# Create the list of fields to be printed.
if COLORFILELIST:
  PRINTLIST = COLORFILELIST
elif (colorset == 'rotate'):
  createrotateprintlist()
else:
  createprintlist()

# Hande color shuffle and invert.
handleshuffle()
handleinvert()

# Decide which fields to skip. The RGB code for these fields is simply replaced with the keyword 'skip'.
handleskips()

# Print color values on request.
handleprintvalues()


# PRINT STUFF

# Add a background layer on request.
if (showbackground == 'yes'):
  l0 = layer('Background')
  bg = writebackground(0, 0, pagewidth, pageheight, backgroundcolor)
  l0.add(bg)
# Add a layer for field output
l1 = layer('Fields')

# For BONUS CODE: print onbjects i.o. fields.
# Number of objects is determined by fieldnum. If less shown than expected, disable fieldskip.
# Cause stuff to appear on the screen.
if (showmesquares == 'yes'):
  printsquares(PRINTLIST)
if (showmerectangles == 'yes'):
  printrectangles(PRINTLIST)
if (showmecircles == 'yes'):
  printcircles(PRINTLIST)
if (showmetriangles == 'yes'):
  printtriangles(PRINTLIST)
if (showmesquares == 'no' and showmerectangles == 'no' and showmecircles == 'no' and showmetriangles == 'no'):
  # Normal output: fields.
  printfields(PRINTLIST)