#########################################################################################
# Package        HiPi::Interface::Seesaw
# Description  : Module for Adafruit seesaw breakouts
# Copyright    : Copyright (c) 2020-2023 Mark Dootson
# License      : This is free software; you can redistribute it and/or modify it under
#                the same terms as the Perl 5 programming language system itself.
#########################################################################################

package HiPi::Interface::Seesaw;

#########################################################################################

use strict;
use warnings;
use parent qw( HiPi::Interface );
use HiPi qw( :rpi :seesaw);
use Carp;
use HiPi::RaspberryPi;
use HiPi::Device::I2C;

use constant {
    EEPROM_MAX_ADDRESS => 0x3E,
    EEPROM_I2C_ADDRESS => 0x3F,
};

my @initaccessors = ( qw(
                    _can_pwm_freq _pwm_width _version _datecode _productcode
                    _auto_flow_control
                    _pwm_ms_per_cycle_A _pwm_true_freq_A
                    _pwm_ms_per_cycle_B _pwm_true_freq_B
                    _options _hardware_id
                    _neopixel_pin _neopixel_colourmap _neopixel_bpp
                    _neopixel_col_byte_map _neopixel_brightness
                    _neopixel_buffer _neopixel_pixels 
                     ) );

__PACKAGE__->create_accessors( qw(  address devicename board action_delay reset_delay pinmap ) );

__PACKAGE__->create_accessors( @initaccessors );

our $VERSION ='0.85';

sub new {
    my ($class, %userparams) = @_;
    
    my $pi = HiPi::RaspberryPi->new();
    
    my %params = (
        devicename   => ( $pi->board_type == RPI_BOARD_TYPE_1 ) ? '/dev/i2c-0' : '/dev/i2c-1',
        device       => undef,
        action_delay => 500,
        reset_delay  => 500,
        reset   => 0,
        pinmap => {
            gpio => {
                9   => 1,
                10  => 1,
                11  => 1,
                14  => 1,
                15  => 1,
                24  => 1,
                25  => 1,  
            },
            adc => {
                2 => 0,
                3 => 1,
                4 => 2,
                # 5 => 3,
            },
            pwm => {
                #4 => 0,
                5 => 1,
                6 => 2,
                7 => 3,
            },
            irq => {
                8 => 1,
            },
        },
    );
    
    my $board = $userparams{board} || SEESAW_ATSAMD09;
    
    if ( $board == SEESAW_ATSAMD09 ) {
        $params{address} = 0x49,
    } else {
        croak q(Unsupported board. Supported board constants are SEESAW_ATSAMD09);
    }
    
    # get user params
    foreach my $key( keys (%userparams) ) {
        $params{$key} = $userparams{$key};
    }
    
    ## handle legacy delay param
    if (exists($params{delay})) {
        $params{action_delay} = $params{delay};
        delete($params{delay});
    }
    
    unless(defined($params{device})) {
        $params{device} = HiPi::Device::I2C->new(
            address      => $params{address},
            devicename   => $params{devicename},
        );
    }
    
    my $resetrequested = $params{reset},
    
    my $self = $class->SUPER::new(%params);
    
    if( $resetrequested ) {
        $self->software_reset;
    } else {
        $self->_init;
    }
    
    return $self;
}

sub _init {
    my $self = shift;
    
    # set undefined properties
    for my $property( @initaccessors ) {
        $self->$property(undef);
    }
    
    my @vervals = $self->read_register(SEESAW_STATUS_BASE, SEESAW_STATUS_VERSION, 4, 8000);
    my $version = ($vervals[0] << 24) | ( $vervals[1] << 16 ) | ($vervals[2] << 8) | $vervals[3];
    my $productnum = $version >> 16;
    my $datecode = $version & 0xFFFF;
    my $year  = $datecode >> 9;
    my $month = ($datecode >> 5) & 0xF;
    my $day   = $datecode & 0x1F;
    my $datevalue = sprintf('%d%02d%02d', 2000 + $year, $month, $day);
    
    $self->_version($version);
    $self->_datecode($datevalue);
    $self->_productcode($productnum);
    
    # unset the options
    $self->_options(undef);
    $self->_hardware_id(undef);
    
    
    # There are two firmware versions that may be loaded on the seesaw as shipped
    #---------------------------------
    # date value 20171023
    #---------------------------------
    # This is the original version on all seesaw boards shipped before Sept 14, 2020
    # This firmware cannot effectively set PWM frequency, lacks auto flow control
    # and has a bug in the neopixel code for greater than 63 pixels
    #
    # firmware is https://github.com/adafruit/seesaw/releases/tag/1.0.1
    
    #---------------------------------
    # date value 20200831
    #---------------------------------
    # This is the official firmware loaded on boards shipped after Sept 14, 2020
    # which fixes the issues with PWM and Neopixels and has auto flow control
    #
    # I assume firmware is latest official release :
    # https://github.com/adafruit/seesaw/releases/tag/1.1.6
    
    # other firmware versions
    
    #---------------------------------
    # date value 20180109
    #---------------------------------
    # In response to the PWM frequency issue raised at https://github.com/adafruit/seesaw/issues/4
    # A compiled firmware was made available that has this date value.
    # It was never loaded on any seesaw as shipped but needed to be loaded by the user.
    # It fixed the PWM issue. It does not have auto flow control
    #
    # firmware from https://github.com/adafruit/seesaw/files/1616082/seesaw_new_firmware.zip
    # Probably built from master branch around that time
    
    #---------------------------------
    # date value 20190801
    #---------------------------------
    # For my own development with pre Sept 14, 2020 boards I built firmware from official release
    # https://github.com/adafruit/seesaw/releases/tag/1.1.6
    # This fixes the issues with PWM and Neopixels and has auto flow control

    if ($datevalue ge '20190801') {
        $self->_pwm_width(16);
        $self->_can_pwm_freq(1);
        $self->_auto_flow_control(1);
    } elsif ($datevalue ge '20180109') {
        $self->_pwm_width(16);
        $self->_can_pwm_freq(1);
        $self->_auto_flow_control(0);
    } else {
        $self->_pwm_width(8);
        $self->_can_pwm_freq(0);
        $self->_auto_flow_control(0);
    }
    return;
}

sub _check_pin_map {
    my($self, $map, @pins) = @_;
    my $badpins = '';
    my $pinmap = $self->pinmap;
    for my $pin( @pins ) {
        $pin ||= 0;
        if (!exists($pinmap->{$map}->{$pin})) {
            $badpins .= ', ' if $badpins;
            $badpins .= $pin;
        }
    }
    return $badpins;
}

sub _check_pin_map_interrupt {
    my($self,  @pins) = @_;
    my $badpins = '';
    my $pinmap = $self->pinmap;
    for my $pin( @pins ) {
        $pin ||= 0;
        if ( !exists($pinmap->{gpio}->{$pin}) &&
             !exists($pinmap->{adc}->{$pin}) ) {
            $badpins .= ', ' if $badpins;
            $badpins .= $pin;
        }
    }
}

sub _get_pin_mask {
    my ( $self, @pins ) = @_;
    my @cmdbytes = (0,0,0,0);
    for my $pin ( @pins ) {
        croak 'zero or undefined pin' unless( $pin );
        my $byte = 3 - int($pin / 8);
        my $bits = $pin % 8;
        $cmdbytes[$byte] |= ( 1 << $bits );
    }
    return @cmdbytes;
}

sub _map_pin_mask {
    my($self, $bytes, @pins) = @_;
    my @results = ();
    for my $pin ( @pins ) {
        croak 'zero or undefined pin' unless( $pin );
        my $byte = 3 - int($pin / 8);
        my $bits = $pin % 8;
        my $value = 1 << $bits;
        if ( $bytes->[$byte] & $value ) {
            push @results, 1;
        } else {
            push @results, 0;
        }
    }
    return @results;
}

sub read_register {
    my($self, $regbase, $regmember, $numbytes, $delay) = @_;
    $delay //= $self->action_delay;
    $self->device->i2c_write($regbase, $regmember);
    $self->sleep_microseconds( $delay );
    my @vals = $self->device->i2c_read( $numbytes );
    return @vals;
}

sub write_register {
    my($self, $regbase, $regmember, @bytes) = @_;
    $self->device->i2c_write($regbase, $regmember, @bytes);
}

sub get_version { return $_[0]->_version; }
    
sub get_date_code { return $_[0]->_datecode; }
   
sub get_product_code { return $_[0]->_productcode; }

sub get_hardware_id {
    my $self = shift;
    unless( $self->_hardware_id ) {
        my ( $hwid ) = $self->read_register( SEESAW_STATUS_BASE, SEESAW_STATUS_HW_ID, 1 );
        $self->_hardware_id( $hwid );
    }    
    return $self->_hardware_id;
}

sub get_options {
    my $self = shift;
    unless( $self->_options ) {
        my @vals = $self->read_register(SEESAW_STATUS_BASE, SEESAW_STATUS_OPTIONS, 4);
        my $opts = ($vals[0] << 24) | ($vals[1] << 16) | ($vals[2] << 8) | $vals[3];        
        $self->_options( $opts );
    }
    return $self->_options;
}

sub get_option_names {
    my $self = shift;
    
    my @optionnames = (
        undef,      # 0
        'GPIO',      # 1
        'UART0',     # 2
        'UART1',     # 3
        'UART2',     # 4
        'UART3',     # 5
        'UART4',     # 6
        'UART5',     # 7
        'PWM',       # 8
        'ADC',       # 9
        'DAC',       # A
        'INTERRUPT', # B
        'DAP',       # C
        'EEPROM',    # D
        'NEOPIXEL',  # E
        'TOUCH',     # F
    );
    
    my $opts = $self->get_options();
    
    my @strings = ();
    
    for (my $i = 0; $i < @optionnames; $i++) {
        my $string = $optionnames[$i];
        next if !defined($string);
        if( ($opts & ( 1 << $i ) ) > 0 ) {
            push @strings, $string;
        } elsif( $i == SEESAW_EEPROM_BASE ) {
            if (  $self->get_hardware_id == 0x55 ) {
                push @strings, $string;
            }
        }
    }
    
    return ( wantarray ) ? @strings : join(',', @strings);
}

sub software_reset {
    my $self = shift;
    $self->write_register(SEESAW_STATUS_BASE, SEESAW_STATUS_SWRST, 0xFF);
    $self->sleep_milliseconds( $self->reset_delay );
    $self->_init;
    return;
}

#----------------------------------------------------
# GPIO
#----------------------------------------------------

sub gpio_set_pin_mode {
    my($self, @pins) = @_;
    
    my $mode = pop @pins;
    
    if (my $badpins = $self->_check_pin_map('gpio', @pins) ) {
        croak 'Invalid pin numbers for gpio_set_pin_mode : ' . $badpins;
    }
    my @bytes = $self->_get_pin_mask( @pins );
    if ( $mode == SEESAW_OUTPUT) {
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_DIRSET_BULK, @bytes);
    } elsif( $mode == SEESAW_INPUT) {
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_DIRCLR_BULK, @bytes);
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_PULLENCLR , @bytes);
    } elsif( $mode == SEESAW_INPUT_PULLUP) {
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_DIRCLR_BULK, @bytes);
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_PULLENSET , @bytes);
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_SET, @bytes);
    } elsif( $mode == SEESAW_INPUT_PULLDOWN) {
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_DIRCLR_BULK, @bytes);
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_PULLENSET , @bytes);
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_CLR, @bytes);
    } else {
        croak 'Invalid mode for gpio_set_pin_mode';
    }
    $self->sleep_microseconds( $self->action_delay );
    return;
}

sub gpio_set_pin_value {
    my($self, @pins) = @_;
    my $value = pop @pins;
    if (my $badpins = $self->_check_pin_map('gpio', @pins) ) {
        croak 'Invalid pin numbers for gpio_set_pin_value : ' . $badpins;
    }
    my @bytes = $self->_get_pin_mask( @pins );
    if($value) {
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_SET, @bytes);
    } else {
        $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_CLR, @bytes);
    }
}

sub gpio_get_pin_value {
    my($self, @pins) = @_;
    if (my $badpins = $self->_check_pin_map('gpio', @pins) ) {
        croak 'Invalid pin numbers for gpio_get_pin_value : ' . $badpins;
    }
    
    # get all pins
    my @bytes = $self->read_register(SEESAW_GPIO_BASE , SEESAW_GPIO_BULK, 4 );
    my @results = $self->_map_pin_mask(\@bytes, @pins );
    
    return wantarray ? @results : $results[0];
}

sub gpio_toggle_pin_value {
    my($self, @pins) = @_;
    if (my $badpins = $self->_check_pin_map('gpio', @pins) ) {
        croak 'Invalid pin numbers for gpio_toggle_pin_value : ' . $badpins;
    }
    my @bytes = $self->_get_pin_mask( @pins );
    $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_TOGGLE, @bytes);
    carp('toggle not working with seesaw SAMD09');
    return;
}

sub gpio_enable_interrupt {
    my($self, @pins) = @_;
    if (my $badpins = $self->_check_pin_map_interrupt( @pins ) ) {
        croak 'Invalid pin numbers for gpio interrupt : ' . $badpins;
    }
    my @bytes = $self->_get_pin_mask( @pins );
    $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_INTENSET, @bytes);
    $self->sleep_microseconds( $self->action_delay );
    return;
}

sub gpio_disable_interrupt {
    my($self, @pins) = @_;
    if (my $badpins = $self->_check_pin_map_interrupt( @pins ) ) {
        croak 'Invalid pin numbers for gpio interrupt : ' . $badpins;
    }
    my @bytes = $self->_get_pin_mask( @pins );
    $self->write_register(SEESAW_GPIO_BASE, SEESAW_GPIO_INTENCLR, @bytes);
    $self->sleep_microseconds( $self->action_delay );
    return;
}

sub gpio_get_interrupt_flags {
    my($self, @pins) = @_;
    if (my $badpins = $self->_check_pin_map_interrupt( @pins) ) {
        croak 'Invalid pin numbers for gpio_get_pin_interrupt : ' . $badpins;
    }
    
    # get all pins
    my @bytes = $self->read_register(SEESAW_GPIO_BASE , SEESAW_GPIO_INTFLAG, 4 );
    my @results = $self->_map_pin_mask(\@bytes, @pins );
    
    return wantarray ? @results : $results[0];
}

#-------------------------------------------------------
# NEOPIXEL
#-------------------------------------------------------

sub set_neopixel {
    my($self, %userparams) = @_;
    
    my $checkpin = $userparams{pin} || 'MISSING';
    
    if (my $badpins = $self->_check_pin_map('gpio', $checkpin) ) {
        croak 'Invalid pin number for get_neopixel : ' . $badpins;
    }
    
    my %params = (
        pixels     => 1,
        colourmap  => SEESAW_NEOPIXEL_GRBW,
        speed      => SEESAW_NEOPIXEL_KHZ800,
        brightness => 5,
    );
    
    foreach my $key( keys (%userparams) ) {
        $params{$key} = $userparams{$key};
    }
    
    if ($params{colormap}) {
        $params{colourmap} = $params{colormap};
    }
    
    if ( $params{colourmap} == SEESAW_NEOPIXEL_RGB ) {
        $params{bpp} = 3;
        $params{col_byte_map} = [ 0, 1, 2 ];
    } elsif($params{colourmap} == SEESAW_NEOPIXEL_GRB ) {
        $params{bpp} = 3;
        $params{col_byte_map} = [ 1, 0, 2 ];
    } elsif($params{colourmap} == SEESAW_NEOPIXEL_RGBW ) {
        $params{bpp} = 4;
        $params{col_byte_map} = [ 0, 1, 2, 3 ];
    } elsif($params{colourmap} == SEESAW_NEOPIXEL_GRBW ) {
        $params{bpp} = 4;
        $params{col_byte_map} = [ 1, 0, 2, 3 ];
    } else {
        croak 'no valid colourmap provided for neopixel';
    }
    
    my @buffer;
    
    #_neopixel_pin _neopixel_colourmap _neopixel_bpp
    #_neopixel_col_byte_map _neopixel_brightness
    #_neopixel_buffer
    
    for(my $i = 0; $i < $params{pixels}; $i++) {
        $buffer[$i] = [0,0,0,0,0];
    }
    
    $self->_neopixel_pixels( $params{pixels} );
    $self->_neopixel_pin( $params{pin} );
    $self->_neopixel_bpp( $params{bpp} );
    $self->_neopixel_col_byte_map( $params{col_byte_map} );
    $self->_neopixel_buffer( \@buffer );
    $self->_neopixel_brightness( $params{brightness} );
    
    # set pin
    $self->gpio_set_pin_mode($self->_neopixel_pin, SEESAW_OUTPUT);
    $self->write_register(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_PIN, $self->_neopixel_pin);
        
    # speed
    $self->write_register(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SPEED, $params{speed} );
    
    # bufflen
    my $blen = 2 + ($self->_neopixel_pixels * $self->_neopixel_bpp);
    my $msb = ( $blen & 0xFF00 ) >> 8;
    my $lsb = $blen & 0xFF;
    $self->write_register(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF_LENGTH, $msb, $lsb);
    
    return 1;
}

sub neopixel_show {
    my ($self ) = @_;
    return unless($self->_neopixel_pin);
    $self->_flush_neopixel_buffer();
    $self->write_register( SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SHOW );
    $self->sleep_microseconds($self->action_delay);
}

sub neopixel_clear {
    my ($self) = @_;
    my @colour = (0,0,0,0,0);
    for( my $i = 0; $i < $self->_neopixel_pixels; $i++ ) {
        $self->neopixel_set_pixel($i, @colour );
    }
    $self->neopixel_show();
}

sub neopixel_set_brightness {
    my($self, $newval) = @_;
    $newval = 0 if $newval < 0;
    $newval = 100 if $newval > 100;
    $self->_neopixel_brightness( $newval );
}

sub neopixel_set_pixel {
    my($self, $pixel, $r, $g, $b, $w, $l) = @_;
    if ($pixel < 0 || $pixel >= $self->_neopixel_pixels) {
        carp qq(invalid pixel number $pixel);
        return;
    }
    
    $w //= 0;
    $l //= $self->_neopixel_brightness;
    $l = 0 if $l < 0;
    $l = 100 if $l > 100;
    
    my @cols =  ( $r, $g, $b, $w );
    
    for ( @cols) {
        $_ = 0 if $_ < 0;
        $_ = 255 if $_ > 255;
    }
    
    for (my $i = 0; $i < $self->_neopixel_bpp; $i++ ) {
        my $map = $self->_neopixel_col_byte_map->[$i];
        $self->_neopixel_buffer->[$pixel]->[$i] = $cols[$map];
    }
    
    $self->_neopixel_buffer->[$pixel]->[-1] = $l;
}

sub _flush_neopixel_buffer {
    my ($self ) = @_;
    
    for (my $i = 0; $i < @{ $self->_neopixel_buffer }; $i ++) {
        my $pixel = $self->_neopixel_buffer->[$i];
        my $brightness = $pixel->[-1];
        my @bytes = ( 0,  ($i * $self->_neopixel_bpp) & 0xFF );
        for (my $i = 0; $i < $self->_neopixel_bpp; $i++) {
            my $val = $pixel->[$i];
            if ( $brightness < 100 ) {
                $val = int($val * $brightness / 100);
            }
            push @bytes, $val & 0xFF;
        }
        
        $self->write_register(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF, @bytes);
    }
    
    return;
}

#---------------------------------------------------------
# ADC
#---------------------------------------------------------

sub adc_read {
    my($self, $pin) = @_;
    if (my $badpins = $self->_check_pin_map('adc', $pin) ) {
        croak 'Invalid pin number for adc_read : ' . $badpins;
    }
    
    my $adcreg = 0;
    
    if ($pin == SEESAW_PA02) {
        $adcreg = 0x07;
    } elsif($pin == SEESAW_PA03) {
        $adcreg = 0x08;
    } elsif($pin == SEESAW_PA04) {
        $adcreg = 0x09;
    #} elsif($pin == SEESAW_PA05) {
    #    $adcreg = 0x0A;
    } else {
        croak 'Invalid pin number for adc_read : ' . $pin;
    }
    $self->sleep_milliseconds(5) unless $self->_auto_flow_control;
    my($msb, $lsb) = $self->read_register( SEESAW_ADC_BASE, $adcreg, 2 );
    my $result = ($msb << 8) | $lsb;
    return $result & 0x3FF;
}

sub adc_read_v {
    my($self, $pin, $ref) = @_;
    $ref //= 3.3;
    my $tenbit = $self->adc_read($pin);    
    my $result = $tenbit / 1023 * $ref;
    return $result;
}

sub adc_read_percent {
    my($self, $pin) = @_;
    my $tenbit = $self->adc_read($pin);
    my $result = ($tenbit * 100) / 1023;
    return $result;
}

#---------------------------------------------------------
# EEPROM
#---------------------------------------------------------

sub eeprom_read {
    my($self, $address, $numbytes) = @_;
    
    $numbytes ||= 1;
    
    if ( $address < 0 || $address > EEPROM_MAX_ADDRESS ) {
        carp sprintf(qq(invalid eeprom address 0x%X - must be in the range 0x00 to 0x3E), $address);
        return;
    }
    
    my $max_write_bytes = 1 + ( EEPROM_MAX_ADDRESS - $address );
    
    if ( $numbytes > $max_write_bytes ) {
        carp sprintf(qq(too may bytes specified '%s' to read from  eeprom 0x%X - must be in the range 0x00 to 0x3E), $numbytes, $address);
        return;
    }
    
    my @vals = $self->read_register( SEESAW_EEPROM_BASE, $address, $numbytes );
    return wantarray ? @vals : $vals[0];
}

sub eeprom_write {
    my($self, $address, @values) = @_;
    
    if ( $address < 0 || $address > EEPROM_MAX_ADDRESS ) {
        carp sprintf(qq(invalid eeprom address 0x%X - must be in the range 0x00 to 0x3E), $address);
        return 0;
    }
    
    my $numvalues = scalar @values;
        
    my $max_write_bytes = 1 + ( EEPROM_MAX_ADDRESS - $address );
    
    if ( $numvalues > $max_write_bytes) {
        carp q(Too many values for eeprom write would overwrite I2C address.);
        return 0;
    }
    
    $self->write_register( SEESAW_EEPROM_BASE, $address, @values );
    return $numvalues;
}

sub get_i2c_address {
    my($self) = @_;
    my( $i2c_address) = $self->read_register( SEESAW_EEPROM_BASE, EEPROM_I2C_ADDRESS, 1 );
    return $i2c_address;
}

sub set_i2c_address {
    my($self, $value) = @_;
    $self->write_register( SEESAW_EEPROM_BASE, EEPROM_I2C_ADDRESS, $value );
}

#---------------------------------------------------------
# PWM
#---------------------------------------------------------

sub get_pwm_width { return $_[0]->_pwm_width; }

sub pwm_can_set_frequency { return $_[0]->_can_pwm_freq; }

sub _get_pwm_reg_and_timer_for_pin {
    my($self, $pin) = @_;
    
    $pin //= 0; # prevent undef and ==
    
    my ( $pwmreg, $timer );
    
    if ($pin == SEESAW_PA07) {
        $pwmreg = 3;
        $timer = 'B';
    } elsif($pin == SEESAW_PA06) {
        $pwmreg = 2;
        $timer = 'B';
    } elsif($pin == SEESAW_PA05) {
        $pwmreg = 1;
        $timer = 'A';
    } elsif($pin == SEESAW_PA04) {
    #    $pwmreg = 0;
    #    $timer  = 'A';
    } else {
        croak 'Invalid pin number for PWM frequency : ' . $pin;
    }
    
    return ( $pwmreg, $timer );
}

sub pwm_set_frequency {
    my($self, $pin, $value ) = @_;
    
    # PWM pins 4 and 5 share a timer, and PWM pins 6 and 7 share a timer.
    
    if (my $badpins = $self->_check_pin_map('pwm', $pin) ) {
        croak 'Invalid pin number for pwm : ' . $badpins;
    }
    
    if ($value < 0 || $value > 800) {
        croak( 'Value for PWM frequency must be 0 to 720' );
    }
    
    my ( $pwmreg, $timer ) = $self->_get_pwm_reg_and_timer_for_pin( $pin );
            
    #uint8_t prescale = TC_CTRLA_PRESCALER_DIV256_Val;                       = 2.8125HZ
    #    if( freq > 500) prescale = TC_CTRLA_PRESCALER_DIV1_Val;             = 720HZ
    #    else if( freq > 250 ) prescale = TC_CTRLA_PRESCALER_DIV2_Val;       = 360HZ
    #    else if( freq > 140 ) prescale = TC_CTRLA_PRESCALER_DIV4_Val;       = 180HZ
    #    else if( freq > 75 ) prescale = TC_CTRLA_PRESCALER_DIV8_Val;        = 90HZ
    #    else if( freq > 25 ) prescale = TC_CTRLA_PRESCALER_DIV16_Val;       = 45HZ
    #    else if( freq > 7 ) prescale = TC_CTRLA_PRESCALER_DIV64_Val;        = 11.25HZ
    
    my ( $truefreq, $mspercycle );
    
    unless( $self->pwm_can_set_frequency ) {
        # record default frequencies
        $truefreq = 48000000 / ( ( 65535 + 1024 ) / 4 );
        $mspercycle = 1000000 / $truefreq;
        my $carp = 'Cannot set frequency with firmware dated ' . $self->get_date_code;
        $carp .= qq(\nFrequency is $truefreq HZ\nMicroseconds Per Cycle is $mspercycle us);
        carp $carp;
        if ( $timer eq 'A') {
            $self->_pwm_ms_per_cycle_A( $mspercycle );
            $self->_pwm_true_freq_A( $truefreq );
        } elsif ( $timer eq 'B') {
            $self->_pwm_ms_per_cycle_B( $mspercycle );
            $self->_pwm_true_freq_B( $truefreq );
        } else {
            croak 'unable to determine timer for pin';
        }
        return $truefreq;
    }
    
    my $psdivider = 256;
    if ( $value > 500 ) {
        $psdivider = 1;
    } elsif ( $value > 250 ) {
        $psdivider = 2;
    } elsif ( $value > 140 ) {
        $psdivider = 4;
    } elsif ( $value > 75 ) {
        $psdivider = 8;
    } elsif ( $value > 25 ) {
        $psdivider = 16;
    } elsif ( $value > 7 ) {
        $psdivider = 64;
    }
    
    $truefreq = 48000000 / ( $psdivider * ( 65535 + 1024 ) );
    $mspercycle = 1000000 / $truefreq;
        
    if ( $timer eq 'A') {
        $self->_pwm_ms_per_cycle_A( $mspercycle );
        $self->_pwm_true_freq_A( $truefreq );
    } elsif ( $timer eq 'B') {
        $self->_pwm_ms_per_cycle_B( $mspercycle );
        $self->_pwm_true_freq_B( $truefreq );
    } else {
        croak 'unable to determine timer for pin';
    }
        
    my $msb = ($value >> 8) & 0xFF;
    my $lsb = $value & 0xFF;
    
    $self->write_register(SEESAW_TIMER_BASE, SEESAW_TIMER_FREQ, $pwmreg, $msb, $lsb );
    
    return $truefreq;
}

sub pwm_get_frequency {
    my($self, $pin) = @_;
    
    if (my $badpins = $self->_check_pin_map('pwm', $pin) ) {
        croak 'Invalid pin number for pwm : ' . $badpins;
    }
    
    if (!$self->pwm_can_set_frequency) {
        #code
        my $truefreq = 48000000 / ( ( 65535 + 1024 ) / 4 );
        return $truefreq;
    }

    my ( $pwmreg, $timer ) = $self->_get_pwm_reg_and_timer_for_pin( $pin );
    
    my $frequency = 0;
    
    if ( $timer eq 'A') {
        $frequency = $self->_pwm_true_freq_A();
    } elsif ( $timer eq 'B') {
        $frequency = $self->_pwm_true_freq_B();
    }
    
    # return zero if we have not set frequency
    $frequency ||= 0;
        
    return $frequency;
}

sub pwm_get_micros_per_cycle {
    my($self, $pin) = @_;
    
    if (my $badpins = $self->_check_pin_map('pwm', $pin) ) {
        croak 'Invalid pin number for pwm : ' . $badpins;
    }
    
    my ( $pwmreg, $timer ) = $self->_get_pwm_reg_and_timer_for_pin( $pin );
    
    my $us = 0;
    
    if ( $timer eq 'A') {
        $us = $self->_pwm_ms_per_cycle_A();
    } elsif ( $timer eq 'B') {
        $us = $self->_pwm_ms_per_cycle_B();
    }
    
    return $us;
}

sub pwm_set_duty_cycle {
    my($self, $pin, $value) = @_;
    
    if (my $badpins = $self->_check_pin_map('pwm', $pin) ) {
        croak 'Invalid pin number for pwm : ' . $badpins;
    }
    
    $value &= 0xFFFF;
    
    if ($value > 65535) {
        $value = 65535;
    } elsif( $value < 0 ) {
        $value = 0;
    }
    
    my ( $pwmreg, $timer ) = $self->_get_pwm_reg_and_timer_for_pin( $pin );
    
    my $msb = ($value >> 8) & 0xFF;
    my $lsb = $value & 0xFF;
    
    if ( $self->get_pwm_width == 16 ) {   
        $self->write_register(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, $pwmreg, $msb, $lsb );
    } else {
        $self->write_register(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, $pwmreg, $msb );
    }
    return $value;
}

sub pwm_set_pulse_width {
    my( $self, $pin, $microseconds) = @_;
    
    if (my $badpins = $self->_check_pin_map('pwm', $pin) ) {
        croak 'Invalid pin number for pwm : ' . $badpins;
    }
    
    my $dutycycle;
    
    my ( $pwmreg, $timer ) = $self->_get_pwm_reg_and_timer_for_pin( $pin );
    
    if ( $timer eq 'A') {
        $dutycycle = int( 0.5 + (( 65535 / $self->_pwm_ms_per_cycle_A) * $microseconds));
    } elsif ( $timer eq 'B') {
        $dutycycle = int( 0.5 + (( 65535 / $self->_pwm_ms_per_cycle_B) * $microseconds));
    } else {
        croak 'unable to determine timer for pin';
    }
    
    if ($dutycycle > 65535) {
        $dutycycle = 65535;
    } elsif( $dutycycle < 0 ) {
        $dutycycle = 0;
    }
    
    my $rval = $self->pwm_set_duty_cycle($pin, $dutycycle );
    
    return $rval;
}

1;

__END__
