#!/usr/local/bin/python

### prion.py (v01.03.01) - An extensible framework for LGP
### 
### Copyright (C) 2018 Christopher J. Crowe
###
### 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 <https://www.gnu.org/licenses/>.

import random

PRION_NOISY   = False
PRION_LIBTEST = True  # UPDATE_TAG <Make testing code less lengthy>

# There is usually a minimal set of functions for the generated program to use.
# Can't imagine many reasons to not assume they will be included
# unless you imagine a good reason to do so, you need these for an actual system
# To really function at all. If you need for some reason to use any inputs than you can
# override the default behavior to produce thise results

# The "machine" (or processor), at its core is a simulated stack processor. In certain ares, stack-based machines demonstrate
# superior performance. In most of the remaineder they seem to function no better or worse.
# I'm sure I'll rant about it on the blog a bit more, so here is the naming scheme that will be utilized
# as the registers of this stack-based VM. This in the final implementation of the processor, so at this point in the design,
# we need to be careful about implementation decision. I want prion to almost boot-strap itself up so the priomordial
# functions are going to be instantiated as fully-realized and executable functions for the interpreter. I do want some flexibility
# at this point. But this is a very important point and bears some dwelling upon
# -- At this point we can assume that we will get an array of objects at a minimun.
# -- This object are capable of interpreting some intrinsic things or concepts. The
# -- "push" and "pop" of the stack operations. Also assume the wire is a RO bus from which a single value is transmitted
# -- We will assume that a stack should always contain a zero

# Should you find any collisions in your local namespace for some odd reason, you can globally alter
# the naming conventions from here and the system can bootstrap itself out of you namespace
PRION_OUTPUT_NAME = "PRION_STACK"
PRION_INPUT_NAME  = "PRION_WIRE"

# These next two will "feel" a tad strange, but they are part of the bootstapping process for this system
# The code will bootstrap itself everytime it comes up  so pay attention if you're changing anything.
# We are utilizing python's native reflectivity to compile the basic code into python functions which will
# then by used within the program itself to interpret the prion sequences as programs operating on a
# stack based processor.
# This may seem like overkill but we will show in later iterations how to utilize a "Compiler" object to
# create functional programs in many different languages. The initial implementations revolve around
# "prion" the language. The reason I have chosen to generate prion sequences as opposed to just generating
# everything in python directly (since we are utilizing reflection anyway) is two-fold:
# -- prion is easily translated to python anyway
# -- prion (and its underlying processor architecture) are easy to migrate to other languages as needed

PRION_TAG_DELIMITER = "&"
PRION_OUTPUT_LBL    = PRION_TAG_DELIMITER + "OUT"
PRION_INPUT_LBL     = PRION_TAG_DELIMITER + "WIRE"
PRION_FUNCTION_LBL  = PRION_TAG_DELIMITER + "FUNC"
PRION_LABEL_ARRAY   = [PRION_OUTPUT_LBL, PRION_INPUT_LBL, PRION_FUNCTION_LBL]

if PRION_NOISY:
    print("PRION_CORE::Library initialized with:")
    print("PRION_LABEL_ARRAY = " + str(PRION_LABEL_ARRAY))
    print(" -- PRION_TAG_DELIMITER = " + PRION_TAG_DELIMITER)
    print(" -- PRION_OUTPUT_LBL    = " + PRION_OUTPUT_LBL)
    print(" -- PRION_INPUT_LBL     = " + PRION_INPUT_LBL)
    print(" -- PRION_FUNCTION_LBL  = " + PRION_FUNCTION_LBL)
    print("---------------------------------------------")
    
# Now we actually start writing the code
PRION_OPCODE_INT = -1    # UPDATE_TAG <Add Interrupt vector to next major version>
PRION_OPCODE_NOP =  0

# prion_INP creation here
PRION_OPCODE_INP        = PRION_OPCODE_NOP + 1
PRION_INP_FUNCTION_TEMP = """
def &FUNC(&OUT , &WIRE):
    &OUT.append(&WIRE)
    return
"""
PRION_INP_FUNCTION_DEF = PRION_INP_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_INP")
PRION_INP_FUNCTION_DEF = PRION_INP_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")
PRION_INP_FUNCTION_DEF = PRION_INP_FUNCTION_DEF.replace(PRION_INPUT_LBL, "wire")

if PRION_NOISY:
    print("PRION_CORE::prion_INP::Defined as below")
    print(PRION_INP_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_INP))
    print("---------------------------------------------")

#############################    
# Indentaton matters here   #
# please be careful if      #
# this is moved around      #    
#############################
exec(PRION_INP_FUNCTION_DEF)#
################%############

if PRION_LIBTEST:
    print("PRION_CORE::prion_INP::Compiled...testing")
    x = []
    print(str(x))
    prion_INP(x, 1)
    print(str(x))
    prion_INP(x, 2)
    print(str(x))
    prion_INP(x, 3)
    print(str(x))
    print("---------------------------------------------")
    
    
# prion_PUSH creation here
PRION_OPCODE_PUSH        = PRION_OPCODE_INP + 1
PRION_PUSH_FUNCTION_TEMP = """
def &FUNC(&OUT , &WIRE):
    &OUT.append(&WIRE)
    return
"""
PRION_PUSH_FUNCTION_DEF = PRION_PUSH_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_PUSH")
PRION_PUSH_FUNCTION_DEF = PRION_PUSH_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")
PRION_PUSH_FUNCTION_DEF = PRION_PUSH_FUNCTION_DEF.replace(PRION_INPUT_LBL, "erc")

if PRION_NOISY:
    print("PRION_CORE::prion_PUSH::Defined as below")
    print(PRION_PUSH_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_PUSH))
    print("---------------------------------------------")

##############################    
# Indentaton matters here    #
# please be careful if       #
# this is moved around       #
##############################
exec(PRION_PUSH_FUNCTION_DEF)#
##############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_PUSH::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    print("---------------------------------------------")


# prion_POP creation here
PRION_OPCODE_POP        = PRION_OPCODE_PUSH + 1
PRION_POP_FUNCTION_TEMP = """
def &FUNC(&OUT):
    if len(&OUT) > 0:
        &OUT.pop()
    if len(&OUT) == 0:
        &OUT.append(0)
        return
    return
"""
PRION_POP_FUNCTION_DEF = PRION_POP_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_POP")
PRION_POP_FUNCTION_DEF = PRION_POP_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")


if PRION_NOISY:
    print("PRION_CORE::prion_POP::Defined as below")
    print(PRION_POP_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_POP))
    print("---------------------------------------------")

#############################    
# Indentaton matters here   #
# please be careful if      #
# this is moved around      #
#############################
exec(PRION_POP_FUNCTION_DEF)#
#############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_POP::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    prion_POP(x)
    print(str(x))
    prion_POP(x)
    print(str(x))
    prion_POP(x)
    print(str(x))
    print("---------------------------------------------")
    print("PRION_CORE::Explicit test of empty stack pops")
    prion_POP(x)
    print(str(x))
    prion_POP(x)
    print(str(x))
    prion_POP(x)
    print(str(x))
    print("---------------------------------------------")
    

# Arithmetic Functions
# Now we build our more complex arithmetic  prion functions
# -- Addition
# -- Subtraction
# -- Multiplication
# -- Division
# These functions need to validate the stack and store
# temporary data. Some, like division, will require additional care
# it must be protected from div/0 errors. Also we must ensure that
# all functions leave the stack they operated on in a consistent
# and meaningful state for the next operation (and they will not
# know which operation is theoretically coming next in their raw form)

# prion_ADD creation here
PRION_OPCODE_ADD        = PRION_OPCODE_POP + 1
PRION_ADD_FUNCTION_TEMP = """
def &FUNC(&OUT):
    if len(&OUT) > 1:
        &OUT.append(&OUT.pop() + &OUT.pop())
        return
    if len(&OUT) == 0:
        &OUT.append(0)
        return
    return
"""
PRION_ADD_FUNCTION_DEF = PRION_ADD_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_ADD")
PRION_ADD_FUNCTION_DEF = PRION_ADD_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")


if PRION_NOISY:
    print("PRION_CORE::prion_ADD::Defined as below")
    print(PRION_ADD_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_ADD))
    print("---------------------------------------------")

#############################    
# Indentaton matters here   #
# please be careful if      #
# this is moved around      #
#############################
exec(PRION_ADD_FUNCTION_DEF)#
#############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_ADD::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    prion_ADD(x)
    print(str(x))
    prion_ADD(x)
    print(str(x))
    prion_ADD(x)
    print(str(x))
    print("---------------------------------------------")

# prion_SUB creation here
PRION_OPCODE_SUB        = PRION_OPCODE_ADD + 1
PRION_SUB_FUNCTION_TEMP = """
def &FUNC(&OUT):
    if len(&OUT) > 1:
        &OUT.append(&OUT.pop() - &OUT.pop())
        return
    if len(&OUT) == 0:
        &OUT.append(0)
        return
    return
"""
PRION_SUB_FUNCTION_DEF = PRION_SUB_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_SUB")
PRION_SUB_FUNCTION_DEF = PRION_SUB_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")


if PRION_NOISY:
    print("PRION_CORE::prion_SUB::Defined as below")
    print(PRION_SUB_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_SUB))
    print("---------------------------------------------")

#############################    
# Indentaton matters here   #
# please be careful if      #
# this is moved around      #
#############################
exec(PRION_SUB_FUNCTION_DEF)#
#############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_SUB::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    prion_SUB(x)
    print(str(x))
    prion_SUB(x)
    print(str(x))
    prion_SUB(x)
    print(str(x))
    print("---------------------------------------------")


# prion_MULT creation here
PRION_OPCODE_MULT        = PRION_OPCODE_SUB + 1
PRION_MULT_FUNCTION_TEMP = """
def &FUNC(&OUT):
    if len(&OUT) > 1:
        &OUT.append(&OUT.pop() * &OUT.pop())
        return
    if len(&OUT) == 0:
        &OUT.append(0)
        return
    return
"""
PRION_MULT_FUNCTION_DEF = PRION_MULT_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_MULT")
PRION_MULT_FUNCTION_DEF = PRION_MULT_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")


if PRION_NOISY:
    print("PRION_CORE::prion_MULT::Defined as below")
    print(PRION_MULT_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_MULT))
    print("---------------------------------------------")

##############################    
# Indentaton matters here    #
# please be careful if       #
# this is moved around       #
##############################
exec(PRION_MULT_FUNCTION_DEF)#
##############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_MULT::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    prion_MULT(x)
    print(str(x))
    prion_MULT(x)
    print(str(x))
    prion_MULT(x)
    print(str(x))
    print("---------------------------------------------")

# prion_DIV creation here
PRION_OPCODE_DIV        = PRION_OPCODE_MULT + 1
PRION_DIV_FUNCTION_TEMP = """
def &FUNC(&OUT):
    if len(&OUT) > 1:
        x1 = &OUT.pop() * 1.0
        x2 = &OUT.pop() * 1.0
        if x2 == 0:
            &OUT.append(x1)
            &OUT.append(x2)
            return
        &OUT.append(x1 // x2)
        return
    if len(&OUT) == 0:
        &OUT.append(0)
        return
    return
"""
PRION_DIV_FUNCTION_DEF = PRION_DIV_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_DIV")
PRION_DIV_FUNCTION_DEF = PRION_DIV_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")


if PRION_NOISY:
    print("PRION_CORE::prion_DIV::Defined as below")
    print(PRION_DIV_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_DIV))
    print("---------------------------------------------")

#############################    
# Indentaton matters here   #
# please be careful if      #
# this is moved around      #
#############################
exec(PRION_DIV_FUNCTION_DEF)#
#############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_DIV::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    prion_DIV(x)
    print(str(x))
    prion_DIV(x)
    print(str(x))
    prion_DIV(x)
    print(str(x))
    print("---------------------------------------------")
    print("PRION_CORE::prion_DIV::Explicit Div/0 check")
    x = []
    print(str(x))
    prion_PUSH(x, 0)
    print(str(x))
    prion_PUSH(x, 5)
    print(str(x))
    prion_DIV(x)
    print(str(x))
    print(str(x))
    prion_DIV(x)
    print(str(x))
    print("---------------------------------------------")


# prion_IF creation here
PRION_OPCODE_IF        = PRION_OPCODE_DIV + 1
PRION_IF_FUNCTION_TEMP = """
def &FUNC(&OUT):
    if len(&OUT) > 2:
        x1 = &OUT.pop()
        x2 = &OUT.pop()
        x3 = &OUT.pop()
        
        if x1 > x2:
            &OUT.append(x1)
            return
        &OUT.append(x3)
        return
    if len(&OUT) == 0:
        &OUT.append(0)
        return
    return
"""
PRION_IF_FUNCTION_DEF = PRION_IF_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_IF")
PRION_IF_FUNCTION_DEF = PRION_IF_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")


if PRION_NOISY:
    print("PRION_CORE::prion_IF::Defined as below")
    print(PRION_IF_FUNCTION_DEF)
    print("Assinged OPCODE: " + str(PRION_OPCODE_IF))
    print("---------------------------------------------")

############################    
# Indentaton matters here  #
# please be careful if     #
# this is moved around     #
############################
exec(PRION_IF_FUNCTION_DEF)#
############################

if PRION_LIBTEST:
    print("PRION_CORE::prion_IF::Compiled...testing")
    x = []
    print(str(x))
    prion_PUSH(x, 1)
    print(str(x))
    prion_PUSH(x, 2)
    print(str(x))
    prion_PUSH(x, 3)
    print(str(x))
    prion_IF(x)
    print(str(x))
    prion_IF(x)
    print(str(x))
    prion_IF(x)
    print(str(x))
    print("---------------------------------------------")


# UPDATE_TAG <Add Boolean Intrinsics>
# UPDATE_TAG <Add Trigonometric Intrinsics>


########################################################################
# You can define your own base functions in the area below
########################################################################

########################################################################
# Here is where we package up our functional primitives nicely for
# the consumption of classes that would have no interest in the above
########################################################################

# Opcodes X Functions (Used by Processors)
PRION_OXF = { PRION_OPCODE_INP:prion_INP, PRION_OPCODE_PUSH:prion_PUSH,
              PRION_OPCODE_POP:prion_POP, PRION_OPCODE_ADD:prion_ADD,
              PRION_OPCODE_SUB:prion_SUB, PRION_OPCODE_MULT:prion_MULT,
              PRION_OPCODE_DIV:prion_DIV, PRION_OPCODE_IF:prion_IF }

# Opcodes X Keywords (Used by Decoders)
PRION_OXK = { PRION_OPCODE_INP:"INP", PRION_OPCODE_PUSH:"PUSH",
              PRION_OPCODE_POP:"POP", PRION_OPCODE_ADD:"ADD",
              PRION_OPCODE_SUB:"SUB", PRION_OPCODE_MULT:"MULT",
              PRION_OPCODE_DIV:"DIV", PRION_OPCODE_IF:"IF" }

# Symbols X Opcodes (Could be useful)
PRION_SXO = { "pop":PRION_OPCODE_POP, "+":PRION_OPCODE_ADD,
              "-":PRION_OPCODE_SUB, "*":PRION_OPCODE_MULT,
              "/":PRION_OPCODE_DIV, "?":PRION_OPCODE_IF }

# Default function set
PRION_FSET = [ PRION_SXO[key] for key in PRION_SXO ]

PRION_ERC_INT = 0
PRION_ERC_DBL = 1
PRION_MUTDEF  = 0.05
PRION_O_IDX   = 0
PRION_S_IDX   = 1
PRION_I_IDX   = 2
PRION_E_IDX   = 2

PRION_F_SLCT  = 1
PRION_I_SLCT  = 2
PRION_E_SLCT  = 2


########################################################################
# The GENOME CLASS is going to be what you will ultimately be using
# to initialize almost every other object in the system. I am keeping 
# it in herently simple.
# -- It has a type of ERC (Ephemoral Random Constant)
# -- It has an intrisic idea of bounds for whatever the ERC requested is
# -- It is capable of generating a single line of LGP code for our stack
#    processor (a codon) and you have at least three available types
#    [INPUT, ERC, FUNCTION]
# You can ask for a specific class of codon, or simply call the basic
# codon() to get one of them randomly (with equal weight
#
# A valid codon follows the three template types. All codons are lists of
# 2 - 3 elements.
# Each of the *codon() functions will give the folowing types of returns
# -- input_codon = [OPCODE, STACK_IDX, INPUT_IDX]
# -- erc_codon   = [OPCODE, STACK_IDX, ERC]
# -- func_codon  = [OPCODE, STACK_IDX]
#
# A list of valid codons of 1 or greater length for the specific genome
# is referred to as a sequence. It follows simple rules
# -- Any sub-sequence of a valid sequence is also a valid sequence.
# -- Two valid sequences (of the same genome) can be cut at any point in
#    their codon lists, and joined together.
# -- A Null list in *not* a valid sequence (I will not code defensively)
# -- If a Null sequence is required for whatever reason, simply create
#    a sequence with 1 NOP instruction.
#
# I append IDX to the stack  and input identifiers since what we could be
# looking at an arry of input but also if solving for multiple outputs
# we can have an arrsy of stacks (the value left of the top of the stack
# after program execution is considered the final value in that case
########################################################################

class Genome:
    def __init__(self, wcount, scount, erc_low, erc_high, fset=PRION_FSET, erc_type=PRION_ERC_INT):
        self.wcount   = wcount
        self.scount   = scount
        self.fset     = fset
        self.erc_type = erc_type
        self.erc_low  = erc_low
        self.erc_high = erc_high

    # -- erc_codon   = [OPCODE, STACK_IDX, ERC]
    def e_codon(self):
        if self.erc_type == PRION_ERC_INT:
            return [PRION_OPCODE_PUSH, random.randint(0, self.scount - 1), random.randint(self.erc_low, self.erc_high)]
        if self.erc_type == PRION_ERC_FLOAT:
            return [PRION_OPCODE_PUSH, random.randint(0, self.scount - 1), random.uniform(self.erc_low, self.erc_high)]
        return [PRION_OPCODE_NOP]

    # -- input_codon = [OPCODE, STACK_IDX, INPUT_IDX]
    def i_codon(self):
        return [PRION_OPCODE_INP, random.randint(0, self.scount - 1), random.randint(0, self.wcount - 1)]

    # -- func_codon  = [OPCODE, STACK_IDX]
    def f_codon(self):
        if len(self.fset) > 0:
            return [random.choice(self.fset), random.randint(0, self.scount - 1)]
        return [PRION_OPCODE_NOP]
        
    def codon(self):
        x = random.choice([PRION_F_SLCT, PRION_I_SLCT, PRION_E_SLCT])
        if x == PRION_E_SLCT:
            return self.e_codon()
        if x == PRION_I_SLCT:
            return self.i_codon()
        if x == PRION_F_SLCT:
            return self.f_codon()
        return [PRION_OPCODE_NOP]

########################################################################
# The SEQUENCER CLASS is where you would put all of your enhanced filters for
# the genrated programs. The library base class is faily strightforward.
#
# Keep requesting codons from the genome until the disired length is reached
# You can inherit and overload the sequence() function in derived classes
# to suit your problem domain as necessary.

# The length is provided as a parameter to the sequence() function.
########################################################################
class Sequencer:
    def __init__(self, genome):
        self.genome = genome

    def sequence(self, length):
        return [self.genome.codon() for x in range(length)] 

########################################################################
# The DECODER CLASS is another simple base class (provided mainly as a 
# convenience to the user / researcher). It enables a sequence of valid
# codons to be slightly more readable. This of it as a reverse assembler
# for the virtual stack processor.
########################################################################
class Decoder:
    def __init__(self, genome):
        self.genome = genome
        
    def decode(self, prog):
        readable = ""
        for x in prog:
            readable += str(PRION_OXK[x[PRION_O_IDX]]) + "\t" + str(x[PRION_O_IDX:]) + "\n" 
        return readable

########################################################################
# The PROCESSOR CLASS is the base implementation of the virtual stack
# processor on which sequences will be run.
#
# It is assumed:
# -- the processor can take an array of inputs (the size of which is
#    contained in the genome)
# -- An array of stack registers (size also specified in the genome)
# -- The base processor takes a prion program and runs the sequence
#    against stacks which start with the initial state [0].
#
# This implies that a stack register in this virtual processor will always
# have an initial value of zero
########################################################################
class Processor:
    def __init__(self, genome):
        self.genome = genome

    def evaluate(self, prog, inputs):
        stacks = [[0] for x in range(self.genome.scount)]

        for line in prog:
            if line[PRION_O_IDX] == PRION_OPCODE_INT:
                pass
            elif line[PRION_O_IDX] == PRION_OPCODE_NOP:
                pass
            elif line[PRION_O_IDX] == PRION_OPCODE_INP:
                stacks[line[PRION_S_IDX]].append(inputs[line[PRION_I_IDX]])
            elif line[PRION_O_IDX] == PRION_OPCODE_PUSH:
                stacks[line[PRION_S_IDX]].append(line[PRION_E_IDX])
            else:
                PRION_OXF[line[PRION_O_IDX]](stacks[line[PRION_S_IDX]])
        return [x[0] for x in stacks]

########################################################################
# The SPLICER CLASS  is where we start seeing more of the biological
# aspect of the prion object model.
#
# The base splicer performs a simple crossover operation. It makes the
# basic assumption that two sequences can be of different lengths.
# To avoid sizing problems it will ensure that it picks a cutpoint that
# will not exceed the bounds of either sequence. This assumes that both
# sequences have been generated with compatable genomes.
#
# Desired bahaviour with sequences of varying lengths can be hard to
# imagine in all cases from the library perspective, so I am leaving it
# tunable by providing a few flags
# -- PRION_TRIM: Generate a sequence of the minimum of the 2 lengths
# -- PRION_BALANCE: Generate a sequence of the average of the 2 lengths 
# -- PRION_GROW: Generate a sequence of the minimum of the 2 lengths
# By default, the base splicer will always trim. This can be modified
# by passing a different flag to the splice() function
########################################################################

PRION_TRIM    = 0
PRION_BALANCE = 1
PRION_GROW    = 2

class Splicer:
    def __init__(self, genome):
        self.genome = genome

    def splice(self, prog1, prog2, mode=PRION_TRIM):
        
        s1 = len(prog1)
        s2 = len(prog2)

        if s1 == s2:
            cutpoint = random.randint(0, s1 - 1)
            return prog1[0:cutpoint] + prog2[cutpoint:]

        # If we get here, we are dealing with sequences that are not
        # of the same length
        if mode == PRION_TRIM:
            if s1 < s2:
                cutpoint = random.randint(0, s1 - 1)
                return prog1[0:cutpoint] + prog2[cutpoint:s1 - 1]
            else: 
                cutpoint = random.randint(0, s2 - 1)
                return prog2[0:cutpoint] + prog1[cutpoint:s2 - 1]
        if mode == PRION_GROW:
            if s1 < s2:
                cutpoint = random.randint(0, s1 - 1)
                return prog1[0:cutpoint] + prog2[cutpoint:]
            else: 
                cutpoint = random.randint(0, s2 - 1)
                return prog2[0:cutpoint] + prog1[cutpoint:]
        if mode == PRION_BALANCE:
            if s1 < s2:
                cutpoint = random.randint(0, s1 - 1)
                return prog1[0:cutpoint] + prog2[cutpoint:s1 + random.randint(s1, s2) - 1]
            else: 
                cutpoint = random.randint(0, s2 - 1)
                return prog2[0:cutpoint] + prog1[cutpoint:s2 + random.randint(s2, s1) - 1]

            
########################################################################
# The MUTATOR CLASS also shows some of its roots in biological/evolutionary
# processes.
#
# It is given a genome and a percentage p. If the
# random.uniform(0.0, 100.0) returns less than or equal to p, the current
# codon is replaced by a random one from the available genome 
########################################################################
class Mutator:
    def __init__(self, genome):
        self.genome = genome

    def mutate(self, seq, p=PRION_MUTDEF):
        idx   = 0
        seqsz = len(seq)
        while seqsz != 0:
            if random.uniform(0.0, 100.0) <= p:
                seq[seqsz -1] = self.genome.codon()
            seqsz -= 1
        return seq

########################################################################
# The FITNESS CLASS determines the overall score of a generated sequence.
#
# By default, the class will simply use a euclidan distance formula
# (although can be extended to cover many more).
# We will make the assumption that the "fitter" a program is
# -- The closer its fitness value will approach 1.0
# -- Overall fitness will be a number between (0.0 - 1.0]
# -- On failure to properly calculate the fitness of an objectit will return -1
#
# Fitness will always be positive in this context
########################################################################
PRION_FIT_EUCLID = 0 # UPDATE_TAG <Add more distance measurement choices>

class Fitness:
    def fitness(self, param, ideal, ftype=PRION_FIT_EUCLID):
        if ftype == PRION_FIT_EUCLID:
            return self.f_euclidian(param, ideal)
        return -1

    def f_euclidian(self, param, ideal):
        if len(param) != len(ideal):
            return -1
        tmp = []
        idx = 0
        while idx <  len(param):
            tmp.append((param[idx] - ideal[idx]) * (param[idx] - ideal[idx]) * 1.0)
            idx += 1
        return (1.0 / (1.0 + sum(tmp)))

def main():
    print("Running test harness")
    print("Please set PRION_NOISY to True at the top of this file (prion.py) for full bootstrap details")
    print("Please set PRION_LIBTEST to True at the top of this file (prion.py) for full bootstrap tests")
    print("---------------------------------------------")
    print("Testing Genome Class")
    g = Genome(5, 2, -5, 5)
    progs = []
    print("Creating list of 20 random ops")
    progs.append( [g.codon() for x in range(20)]) 
    print(str(progs[0]))
    print("Creating list of 20 random ERC ops")
    progs.append( [g.e_codon() for x in range(20)] )
    print(str(progs[1]))
    print("Creating list of 20 random input ops")
    progs.append( [g.i_codon() for x in range(20)] )
    print(str(progs[2]))
    print("Creating list of 20 random function ops")
    progs.append( [g.f_codon() for x in range(20)] )
    print(str(progs[3]))
    print("---------------------------------------------")

    print("Testing Sequencer Class")
    s = Sequencer(g)
    print("Creating list of 500 random programs of 1024 codons in length")
    progs = [s.sequence(1024) for x in range(500)] 
    print("Listing a random programs")
    print(str(random.choice(progs)))
    print("---------------------------------------------")
    
    print("Testing Decoder Class")
    d = Decoder(g)
    print("Decoding Random Program")
    print(d.decode(random.choice(progs)))
    print("---------------------------------------------")

    print("Testing Processor Class")
    p = Processor(g)
    print("Setting inputs to: [100, 20, 43, 91, 16]")
    i = [100, 20, 43, 91, 16]
    pcounter2 = 0
    for prog in progs:
        print("PROGRAM - " + str(pcounter2) + " (output)" + str(p.evaluate(prog, i)))
        pcounter2 += 1
    print("---------------------------------------------")

    print("Testing Splicer Class")
    s = Splicer(g)
    print("First program")
    rand_p1 = random.choice(progs)
    print("Return val: " + str(p.evaluate(rand_p1, i)))
    print("Second  program")
    rand_p2 = random.choice(progs)
    print("Return val: " + str(p.evaluate(rand_p2, i)))
    spliced_p = s.splice(rand_p1, rand_p2)
    print("Spliced program")
    print("Return val: " + str(p.evaluate(spliced_p, i)))
    print("---------------------------------------------")

    print("Testing Mutator Class")
    m = Mutator(g)
    print("Randomly selecting program")
    prog_m = random.choice(progs)
    print(d.decode(prog_m))
    print("Return val: " + str(p.evaluate(prog_m, i)))
    print("Running 25 mutattion passes @ 80% (etremely high in most cases)")
    for x in range(25):
        print("Pass - " + str(x))
        prog_m = m.mutate(prog_m, p=0.8)
        print("Return val: " + str(p.evaluate(prog_m, i)))

    print("Testing Fitness Class")
    f = Fitness()
    print("Randomly selecting 10  programs - testing agins ideal [1, 2]")
    ideal = [1, 2]
    for x in range(10):
        print("Pass - " + str(x))
        result = p.evaluate(random.choice(progs), i)
        print("Return val: " + str(result))
        print("Fitness: " + str(f.fitness(result, ideal))) 
    print("Testing fitness with correct answer")
    print("Fitness: " + str(f.fitness(ideal, ideal))) 
    print("Tests complete")
    print("---------------------------------------------")
    exit()
    
if __name__ == '__main__':
    main()
exit()

# Updates for v01.03.0.1
# Reviewed initial version.
#
# Found the default splice function unfinished (completed in this version):
# - Default splice class has two new flags
# - The splice function takes an additional parameter
#
# Various comment/spelling changes that are non-impactful to functionality.
# - CJC