# -*- coding: utf-8 -*-
"""
---------------------------------------------------------------------------
PocketBeagle Arcade Machine
---------------------------------------------------------------------------
License:   
Copyright 2017 Octavo Systems, LLC

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------
Arcade Machine

  Please see README.txt for overview and how to run the game.
  
  Please use run_arcade_machine.sh for instructions to auto-run the game at 
boot

"""
import os
import random
import time

import Adafruit_BBIO.PWM as PWM

# ------------------------------------------------------------------------
# Global Constants
# ------------------------------------------------------------------------

# HT16K33 values
DISPLAY_I2C_BUS              = 1                 # I2C 1  
DISPLAY_I2C_ADDR             = 0x70
DISPLAY_CMD                  = "/usr/sbin/i2cset -y 1 0x70"         

# Peripheral path
GPIO_BASE_PATH               = "/sys/class/gpio"
ADC_BASE_PATH                = "/sys/bus/iio/devices/iio:device0"

# GPIO direction
IN                           = True
OUT                          = False

# GPIO output state
LOW                          = "0"
HIGH                         = "1"

# Button GPIO values
BUTTON0                      = (1, 27)           # gpio59 / P2_2
BUTTON1                      = (1, 25)           # gpio57 / P2_6
BUTTON2                      = (1, 15)           # gpio47 / P2_18
BUTTON3                      = (1, 14)           # gpio46 / P2_22
BUTTONS                      = [BUTTON0, BUTTON1, BUTTON2, BUTTON3]

# LED GPIO values
LED0                         = (1, 26)           # gpio58 / P2_4
LED1                         = (1, 28)           # gpio60 / P2_8 
LED2                         = (2, 0)            # gpio64 / P2_20
LED3                         = (1, 12)           # gpio44 / P2_24
LEDS                         = [LED0, LED1, LED2, LED3]
  
# Buzzer GPIO value
BUZZER                       = "P2_1"           # gpio50 / (1, 18)

NOTES                        =  [262, 329, 392, 529]

# ------------------------------------------------------------------------
# GPIO/ADC access library
# ------------------------------------------------------------------------

def gpio_setup(gpio, direction, default_value=False):
    """Setup GPIO pin
    
      * Test if GPIO exists; if not create it
      * Set direction
      * Set default value
    """
    gpio_number = str((gpio[0] * 32) + gpio[1])
    path        = "{0}/gpio{1}".format(GPIO_BASE_PATH, gpio_number)
    
    if not os.path.exists(path):
        # "echo {gpio_number} > {GPIO_BASE_PATH}/export"
        print("Create GPIO: {0}".format(gpio_number))
        with open("{0}/export".format(GPIO_BASE_PATH), 'w') as f:
            f.write(gpio_number)
    
    if direction:
        # "echo in > {path}/direction"
        with open("{0}/direction".format(path), 'w') as f:
            f.write("in")
    else:
        # "echo out > {path}/direction"
        with open("{0}/direction".format(path), 'w') as f:
            f.write("out")
        
    if default_value:
        # "echo {default_value} > {path}/value"
        with open("{0}/value".format(path), 'w') as f:
            f.write(default_value)
    
# End def


def gpio_set(gpio, value):
    """Set GPIO ouptut value."""
    gpio_number = str((gpio[0] * 32) + gpio[1])
    path        = "{0}/gpio{1}".format(GPIO_BASE_PATH, gpio_number)
    
    # "echo {value} > {path}/value"
    with open("{0}/value".format(path), 'w') as f:
        f.write(value)

# End def


def gpio_get(gpio):
    """Get GPIO input value."""
    gpio_number = str((gpio[0] * 32) + gpio[1])
    path        = "{0}/gpio{1}".format(GPIO_BASE_PATH, gpio_number)
    
    # "cat {path}/value"
    with open("{0}/value".format(path), 'r') as f:
        out = f.read()
    
    return float(out)

# End def


def adc_get(channel):
    """Get ADC input value.
    
    Returns:
        value (float):  Value will be between 0 (0V) and 1.0 (1.8V)."""
    with open("{0}/{1}".format(ADC_BASE_PATH, channel), 'r') as f:
        out = f.read()
    
    return float(float(out) / float(4096))

# End def

# ------------------------------------------------------------------------
# Display Code
# ------------------------------------------------------------------------
HEX_DIGITS                  = [0x3f, 0x06, 0x5b, 0x4f,    # 0, 1, 2, 3
                               0x66, 0x6d, 0x7d, 0x07,    # 4, 5, 6, 7
                               0x7f, 0x6f, 0x77, 0x7c,    # 8, 9, A, b
                               0x39, 0x5e, 0x79, 0x71]    # C, d, E, F

CLEAR_DIGIT                 = 0x7F
POINT_VALUE                 = 0x80

DIGIT_ADDR                  = [0x00, 0x02, 0x06, 0x08]
COLON_ADDR                  = 0x04
                      
HT16K33_BLINK_CMD           = 0x80
HT16K33_BLINK_DISPLAYON     = 0x01
HT16K33_BLINK_OFF           = 0x00
HT16K33_BLINK_2HZ           = 0x02
HT16K33_BLINK_1HZ           = 0x04
HT16K33_BLINK_HALFHZ        = 0x06

HT16K33_SYSTEM_SETUP        = 0x20
HT16K33_OSCILLATOR          = 0x01

HT16K33_BRIGHTNESS_CMD      = 0xE0
HT16K33_BRIGHTNESS_HIGHEST  = 0x0F
HT16K33_BRIGHTNESS_DARKEST  = 0x00

def display_setup():
    """Setup display"""
    # i2cset -y 0 0x70 0x21
    os.system("{0} {1}".format(DISPLAY_CMD, (HT16K33_SYSTEM_SETUP | HT16K33_OSCILLATOR)))
    # i2cset -y 0 0x70 0x81
    os.system("{0} {1}".format(DISPLAY_CMD, (HT16K33_BLINK_CMD | HT16K33_BLINK_OFF | HT16K33_BLINK_DISPLAYON)))
    # i2cset -y 0 0x70 0xEF
    os.system("{0} {1}".format(DISPLAY_CMD, (HT16K33_BRIGHTNESS_CMD | HT16K33_BRIGHTNESS_HIGHEST)))

# End def


def display_clear():
    """Clear the display to read '0000'"""
    # i2cset -y 0 0x70 0x00 0x3F
    os.system("{0} {1} {2}".format(DISPLAY_CMD, DIGIT_ADDR[0], HEX_DIGITS[0]))
    # i2cset -y 0 0x70 0x02 0x3F
    os.system("{0} {1} {2}".format(DISPLAY_CMD, DIGIT_ADDR[1], HEX_DIGITS[0]))
    # i2cset -y 0 0x70 0x06 0x3F
    os.system("{0} {1} {2}".format(DISPLAY_CMD, DIGIT_ADDR[2], HEX_DIGITS[0]))
    # i2cset -y 0 0x70 0x08 0x3F
    os.system("{0} {1} {2}".format(DISPLAY_CMD, DIGIT_ADDR[3], HEX_DIGITS[0]))
    
    os.system("{0} {1} {2}".format(DISPLAY_CMD, COLON_ADDR, 0x0))
    
# End def


def display_encode(data, double_point=False):
    """Encode data to TM1637 format.
    
    This function will convert the data from decimal to the TM1637 data fromt
    
    :param value: Value must be between 0 and 15
    
    Will throw a ValueError if number is not between 0 and 15.
    """
    ret_val = 0
    
    try:
        if (data != CLEAR_DIGIT):
            if double_point:
                ret_val = HEX_DIGITS[data] + POINT_VALUE
            else:
                ret_val = HEX_DIGITS[data]
    except:
        raise ValueError("Digit value must be between 0 and 15.")

    return ret_val

# End def


def display_set(data):
    """Display the data.
    
    data is a list containing 4 values
    """
    for i in range(0,3):
        display_set_digit(i, data[i])
    
# End def


def display_set_digit(digit_number, data, double_point=False):
    """Update the given digit of the display."""
    os.system("{0} {1} {2}".format(DISPLAY_CMD, DIGIT_ADDR[digit_number], display_encode(data, double_point)))    

# End def


def update_display(value):
    """Update the value on the display.  
    
    This function will clear the display and then set the appropriate digits
    
    :param value: Value must be between 0 and 9999.
    
    Will throw a ValueError if number is not between 0 and 9999.
    """  
    """
    if (value < 0) or (value > 9999):
       raise ValueError("Value is not within 0 and 9999.") 
    
    if (value < 10):
        display_set_digit(3, value)
        display_set_digit(2, 0)
        display_set_digit(1, 0)
        display_set_digit(0, 0)
    else:
        if (value < 100):
            display_set_digit(3, value % 10)
            display_set_digit(2, value / 10)
            display_set_digit(1, 0)
            display_set_digit(0, 0)
    
    """
    for i in range(0,4):
        display_set_digit((3 - i), (value % 10))
        value = (value / 10)
    #"""
    #pass
    

# End def
    
def setup_game():
    "Setup the buttons for the game"
    
    gpio_setup(BUTTON0, IN)
    gpio_setup(BUTTON1, IN)
    gpio_setup(BUTTON2, IN)
    gpio_setup(BUTTON3, IN)    

    gpio_setup(LED0, OUT, LOW)
    gpio_setup(LED1, OUT, LOW)
    gpio_setup(LED2, OUT, LOW)
    gpio_setup(LED3, OUT, LOW)
    
    #gpio_setup(BUZZER, OUT, LOW)
        
    display_setup

# ------------------------------------------------------------------------
# Piezo Buzzer Code
# ------------------------------------------------------------------------

def play_note(note, sec):
    PWM.start(BUZZER, 50, note)
    time.sleep(sec)

# ------------------------------------------------------------------------
# Reflex Tester Code
# ------------------------------------------------------------------------
        
def play_reflex(rand):
    
    gpio_set(LED0, LOW) 
    gpio_set(LED1, LOW)
    #gpio_set(LED2, LOW)
    gpio_set(LED3, LOW)       
            
    time.sleep(random.random() * 6 + 3)      # Wait between 3 and 10 seconds before activating LED
        
    print(rand)
        
    gpio_set(LEDS[rand], HIGH)              # Activate random LED
            
    initial_time = time.time()    
    
    while (gpio_get(BUTTONS[rand]) == 1):   # Wait until correct button is pressed
        pass
    
    #while (gpio_get(BUTTONS[rand]) == 0 and rand == 2):   # Wait until correct button is pressed
    #    print("BUTTON1")
    
    button_press_time = time.time() 
    gpio_set(LEDS[rand], LOW)
        
    if ((button_press_time - initial_time) < 10):
        # Diplay the first three/four digits of the time spent before clicking button
        update_display((int)((button_press_time - initial_time) * 1000)) 
    else:
        display_set_digit(0, 13)            # "D"
        display_set_digit(1, 14)            # "E"
        display_set_digit(2, 10)            # "A"
        display_set_digit(3, 13)            # "D"

# End def

# ------------------------------------------------------------------------
# Simon Says Code
# ------------------------------------------------------------------------

def play_simon_says():
    pattern = []
    user_input = []
    score = 0
    playing_game = True
    
    gpio_set(LED0, LOW) 
    gpio_set(LED1, LOW)
    gpio_set(LED2, LOW)
    gpio_set(LED3, LOW) 
    
    time.sleep(1)
    
    while (playing_game):
        rand = random.randint(0, 3)
        while(rand == 1): #Ignore BUTTON1 (Damaged in original model)
            rand = random.randint(0, 3)
            
        pattern.append(rand)
        print(pattern)
        
        for i in range(0, len(pattern)):
            gpio_set(LEDS[pattern[i]], HIGH)
            play_note(NOTES[pattern[i]], 0.5)
            PWM.stop(BUZZER)
            gpio_set(LEDS[pattern[i]], LOW)
            time.sleep(0.5)
            
        print("LED lights up")
            
        while (len(user_input) < len(pattern)):   # Wait until any button is pressed
            if (gpio_get(BUTTON0) == 0):
                gpio_set(LED0, HIGH)
                user_input.append(0)
                play_note(NOTES[0], 0.5)
                PWM.stop(BUZZER)
                gpio_set(LED0, LOW)
                print("Button 0 accepts input") # TESTING 
            #elif (gpio_get(BUTTON1) == 0):
            #    gpio_set(LED1, HIGH)
            #    user_input.append(1)
            #    time.sleep(0.5)
            #    gpio_set(LED1, LOW)
            #    print("Button 1 accepts input") # TESTING 
            elif (gpio_get(BUTTON2) == 0): #Solved - Add SSH script (config-pin P2_18 gpio)
                gpio_set(LED2, HIGH)
                user_input.append(2)
                play_note(NOTES[2], 0.5)
                PWM.stop(BUZZER)
                gpio_set(LED2, LOW)
                print("Button 2 accepts input") # TESTING 
            elif (gpio_get(BUTTON3) == 0):
                gpio_set(LED3, HIGH)
                user_input.append(3)
                play_note(NOTES[3], 0.5)
                PWM.stop(BUZZER)
                gpio_set(LED3, LOW)
                print("Button 3 accepts input") # TESTING 
        
        for i in range(0, len(pattern)):
            if (pattern[i] != user_input[i]):
                playing_game = False  
                break
            else:
                score += 1
                update_display(score)
            
                    
        user_input[:] = []

# End def
    
# ------------------------------------------------------------------------
# Main script
# ------------------------------------------------------------------------
        
if __name__ == '__main__':
    display_setup()
    display_clear()
    
    setup_game()
    
    play_note(NOTES[0], 0.25)    #Do
    gpio_set(LED0, HIGH)
    play_note(NOTES[1], 0.25)    #Mi
    gpio_set(LED2, HIGH)
    play_note(NOTES[2], 0.25)    #Sol
    gpio_set(LED3, HIGH)
    play_note(NOTES[3], 0.25)    #Do
    PWM.stop(BUZZER)
    
    gpio_set(LED3, LOW)
    
    playing = True
    
    while(playing):
        
        gpio_set(LED0, HIGH)
        gpio_set(LED2, HIGH)
            
        confirm = 0
        
        while ((gpio_get(BUTTON0) == 1) and (gpio_get(BUTTON2) == 1)):   # Wait until correct button is pressed
            pass
        
        if (gpio_get(BUTTON0) == 0):
            gpio_set(LED2, LOW)
            update_display(1)
            time.sleep(0.5)
            
            while (gpio_get(BUTTON0) == 1):
                gpio_set(LED0, LOW)
                time.sleep(0.5)
                gpio_set(LED0, HIGH)
                time.sleep(0.5)
                
                confirm += 1
                if (confirm >= 5):
                    break
            
            display_clear()
                
            r = random.randint(0, 3) 
            while (r == 1):                      #Ignore BUTTON1 (damaged in original model)
                r = random.randint(0, 3)
            
            if (confirm < 5):
                play_reflex(r)
            
        elif (gpio_get(BUTTON2) == 0):
            update_display(2)
            gpio_set(LED0, LOW)
            time.sleep(0.5)
            while (gpio_get(BUTTON2) == 1):
                gpio_set(LED2, LOW)
                time.sleep(0.5)
                gpio_set(LED2, HIGH)
                time.sleep(0.5)
                
                confirm += 1
                if (confirm >= 5):
                    break
            
            display_clear()
            
            if (confirm < 5):
                play_simon_says()
        
        for i in range(0, 3):
            gpio_set(LED0, HIGH) 
            #gpio_set(LED1, HIGH)
            gpio_set(LED2, HIGH)
            gpio_set(LED3, HIGH) 
            time.sleep(0.5)
            gpio_set(LED0, LOW) 
            #gpio_set(LED1, LOW)
            gpio_set(LED2, LOW)
            gpio_set(LED3, LOW) 
            time.sleep(0.5)
    