/*
 * peano.c
 *
 * Basic peano arithmetic
 *
 * (c) 2017 Simon Brooke <simon@journeyman.cc>
 * Licensed under GPL version 2.0, or, at your option, any later version.
 */

#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "consspaceobject.h"
#include "conspage.h"
#include "equal.h"
#include "integer.h"
#include "intern.h"
#include "lispops.h"
#include "print.h"
#include "read.h"
#include "real.h"
#include "stack.h"

long double to_long_double( struct cons_pointer arg );
long int to_long_int( struct stack_frame *frame, struct cons_pointer arg );
struct cons_pointer add_2( struct stack_frame *frame, struct cons_pointer arg1,
                           struct cons_pointer arg2 );


bool zerop( struct cons_pointer arg ) {
    bool result = false;
    struct cons_space_object cell = pointer2cell( arg );

    switch ( cell.tag.value ) {
        case INTEGERTV:
            result = cell.payload.integer.value == 0;
            break;
        case RATIOTV:
            result = zerop( cell.payload.ratio.dividend );
            break;
        case REALTV:
            result = ( cell.payload.real.value == 0 );
            break;
    }

    return result;
}

/**
 * TODO: cannot throw an exception out of here, which is a problem
 * if a ratio may legally have zero as a divisor, or something which is
 * not a number is passed in.
 */
long double to_long_double( struct cons_pointer arg ) {
    long double result = 0;     /* not a number, as a long double */
    struct cons_space_object cell = pointer2cell( arg );

    switch ( cell.tag.value ) {
        case INTEGERTV:
            result = ( double ) cell.payload.integer.value;
            break;
        case RATIOTV:
            {
                struct cons_space_object dividend =
                    pointer2cell( cell.payload.ratio.dividend );
                struct cons_space_object divisor =
                    pointer2cell( cell.payload.ratio.divisor );

                result =
                    ( long double ) dividend.payload.integer.value /
                    divisor.payload.integer.value;
            }
            break;
        case REALTV:
            result = cell.payload.real.value;
            break;
        default:
            result = NAN;
            break;
    }

    fputws( L"to_long_double( ", stderr );
    print( stderr, arg );
    fwprintf( stderr, L") => %lf\n", result );

    return result;
}


/**
 * TODO: cannot throw an exception out of here, which is a problem
 * if a ratio may legally have zero as a divisor, or something which is
 * not a number (or is a big number) is passed in.
 */
long int to_long_int( struct stack_frame *frame, struct cons_pointer arg ) {
    long int result = 0;
    struct cons_space_object cell = pointer2cell( arg );
    switch ( cell.tag.value ) {
        case INTEGERTV:
            result = cell.payload.integer.value;
            break;
        case RATIOTV:
            result = lroundl( to_long_double( arg ) );
            break;
        case REALTV:
            result = lroundl( cell.payload.real.value );
            break;
    }
    return result;
}

long int greatest_common_divisor( long int m, long int n ) {
    int o;
    while ( m ) {
        o = m;
        m = n % m;
        n = o;
    }

    return o;
}

long int least_common_multiple( long int m, long int n ) {
    return m / greatest_common_divisor( m, n ) * n;
}

/**
 * return a cons_pointer indicating a number which is the sum of
* the ratios indicated by `arg1` and `arg2`. If you pass non-ratios,
* this is going to break horribly.
*/
struct cons_pointer add_ratio_ratio( struct stack_frame *frame,
                                     struct cons_pointer arg1,
                                     struct cons_pointer arg2 ) {
    fputws( L"add_ratio_ratio( arg1 = ", stderr );
    print( stderr, arg1 );
    fputws( L"; arg2 = ", stderr );
    print( stderr, arg2 );

    struct cons_pointer result;
    struct cons_space_object cell1 = pointer2cell( arg1 );
    struct cons_space_object cell2 = pointer2cell( arg2 );
    long int dd1v =
        pointer2cell( cell1.payload.ratio.dividend ).payload.integer.value,
        dd2v =
        pointer2cell( cell2.payload.ratio.dividend ).payload.integer.value,
        dr1v =
        pointer2cell( cell1.payload.ratio.divisor ).payload.integer.value,
        dr2v =
        pointer2cell( cell2.payload.ratio.divisor ).payload.integer.value,
        lcm = least_common_multiple( dr1v, dr2v ),
        m1 = lcm / dr1v, m2 = lcm / dr2v;

    fwprintf( stderr, L"); lcm = %ld; m1 = %ld; m2 = %ld", lcm, m1, m2 );

    if ( dr1v == dr2v ) {
        result =
            make_ratio( frame,
                        make_integer( dd1v + dd2v ),
                        cell1.payload.ratio.divisor );

        long int ddrv =
            pointer2cell( pointer2cell( result ).payload.ratio.
                          dividend ).payload.integer.value, drrv =
            pointer2cell( pointer2cell( result ).payload.ratio.
                          divisor ).payload.integer.value, gcd =
            greatest_common_divisor( ddrv, drrv );

        if ( gcd > 1 ) {
            if ( drrv / gcd == 1 ) {
                result = make_integer( ddrv / gcd );
            } else {
                result =
                    make_ratio( frame, make_integer( ddrv / gcd ),
                                make_integer( drrv / gcd ) );
            }
        }
    } else {
        result = add_ratio_ratio( frame,
                                  make_ratio( frame,
                                              make_integer( dd1v * m1 ),
                                              make_integer( dr1v * m1 ) ),
                                  make_ratio( frame,
                                              make_integer( dd2v * m2 ),
                                              make_integer( dr2v * m2 ) ) );
    }

    fputws( L" => ", stderr );
    print( stderr, result );
    fputws( L"\n", stderr );

    return result;
}


/**
 * return a cons_pointer indicating a number which is the sum of
* the ratios indicated by `arg1` and `arg2`. If you pass non-ratios,
* this is going to break horribly.
*/
struct cons_pointer add_integer_ratio( struct stack_frame *frame,
                                       struct cons_pointer intarg,
                                       struct cons_pointer ratarg ) {
    return add_ratio_ratio( frame,
                            make_ratio( frame, intarg, make_integer( 1 ) ),
                            ratarg );
}


/**
* return a cons_pointer indicating a number which is the sum of
* the numbers indicated by `arg1` and `arg2`.
*/
struct cons_pointer add_2( struct stack_frame *frame, struct cons_pointer arg1,
                           struct cons_pointer arg2 ) {
    struct cons_pointer result;
    struct cons_space_object cell1 = pointer2cell( arg1 );
    struct cons_space_object cell2 = pointer2cell( arg2 );

    fputws( L"add_2( arg1 = ", stderr );
    print( stderr, arg1 );
    fputws( L"; arg2 = ", stderr );
    print( stderr, arg2 );

    if ( zerop( arg1 ) ) {
        result = arg2;
    } else if ( zerop( arg2 ) ) {
        result = arg1;
    } else {

        switch ( cell1.tag.value ) {
            case EXCEPTIONTV:
                result = arg1;
                break;
            case INTEGERTV:
                switch ( cell2.tag.value ) {
                    case EXCEPTIONTV:
                        result = arg2;
                        break;
                    case INTEGERTV:
                        result = make_integer( cell1.payload.integer.value +
                                               cell2.payload.integer.value );
                        break;
                    case RATIOTV:
                        result = add_integer_ratio( frame, arg1, arg2 );
                        break;
                    case REALTV:
                        result =
                            make_real( to_long_double( arg1 ) +
                                       to_long_double( arg2 ) );
                        break;
                    default:
                        result = lisp_throw( c_string_to_lisp_string
                                             ( "Cannot add: not a number" ),
                                             frame );
                }
                break;
            case RATIOTV:
                switch ( cell2.tag.value ) {
                    case EXCEPTIONTV:
                        result = arg2;
                        break;
                    case INTEGERTV:
                        result = add_integer_ratio( frame, arg2, arg1 );
                        break;
                    case RATIOTV:
                        result = add_ratio_ratio( frame, arg1, arg2 );
                        break;
                    case REALTV:
                        result =
                            make_real( to_long_double( arg1 ) +
                                       to_long_double( arg2 ) );
                        break;
                }
                break;
            case REALTV:
                result =
                    make_real( to_long_double( arg1 ) +
                               to_long_double( arg2 ) );
                break;
            default:
                result = lisp_throw( c_string_to_lisp_string
                                     ( "Cannot add: not a number" ), frame );
        }
    }

    fputws( L"}; => ", stderr );
    print( stderr, arg2 );
    fputws( L"\n", stderr );


    return result;
}

/**
 * Add an indefinite number of numbers together
 * @param env the evaluation environment - ignored;
 * @param frame the stack frame.
 * @return a pointer to an integer or real.
 */
struct cons_pointer lisp_add( struct stack_frame
                              *frame, struct
                              cons_pointer env ) {
    struct cons_pointer result = make_integer( 0 );
    for ( int i = 0;
          i < args_in_frame &&
          !nilp( frame->arg[i] ) && !exceptionp( result ); i++ ) {
        result = add_2( frame, result, frame->arg[i] );
    }

    struct cons_pointer more = frame->more;
    while ( consp( more ) && !exceptionp( result ) ) {
        result = add_2( frame, result, c_car( more ) );
        more = c_cdr( more );
    }

    return result;
}

/**
 * Internal guts of multiply. Dark and mysterious.
 */
struct cons_pointer multiply_accumulate( struct
                                         cons_pointer arg, struct
                                         stack_frame
                                         *frame, long
                                         int
                                         *i_accumulator, long
                                         double
                                         *d_accumulator, int
                                         *is_int ) {
    struct cons_pointer result = NIL;
    struct cons_space_object cell = pointer2cell( arg );
    switch ( cell.tag.value ) {
        case INTEGERTV:
            ( *i_accumulator ) *= cell.payload.integer.value;
            ( *d_accumulator ) *= numeric_value( arg );
            break;
        case REALTV:
            ( *d_accumulator ) *= cell.payload.real.value;
            ( *is_int ) &= false;
            break;
        case EXCEPTIONTV:
            result = arg;
            break;
        default:
            result =
                lisp_throw
                ( c_string_to_lisp_string
                  ( "Cannot multiply: not a number" ), frame );
    }
    return result;
}

/**
 * Multiply an indefinite number of numbers together
 * @param env the evaluation environment - ignored;
 * @param frame the stack frame.
 * @return a pointer to an integer or real.
 */
struct cons_pointer lisp_multiply( struct
                                   stack_frame
                                   *frame, struct
                                   cons_pointer env ) {
    struct cons_pointer result = NIL;
    long int i_accumulator = 1;
    long double d_accumulator = 1;
    int is_int = true;
    for ( int i = 0; i < args_in_frame && !nilp( frame->arg[i] )
          && !exceptionp( result ); i++ ) {
        result =
            multiply_accumulate( frame->arg[i],
                                 frame,
                                 &i_accumulator, &d_accumulator, &is_int );
    }

    struct cons_pointer more = frame->more;
    while ( consp( more )
            && !exceptionp( result ) ) {
        result =
            multiply_accumulate( c_car
                                 ( more ),
                                 frame,
                                 &i_accumulator, &d_accumulator, &is_int );
        more = c_cdr( more );
    }

    if ( !exceptionp( result ) ) {
        if ( is_int ) {
            result = make_integer( i_accumulator );
        } else {
            result = make_real( d_accumulator );
        }
    }

    return result;
}

/**
 * Subtract one number from another.
 * @param env the evaluation environment - ignored;
 * @param frame the stack frame.
 * @return a pointer to an integer or real.
 */
struct cons_pointer lisp_subtract( struct
                                   stack_frame
                                   *frame, struct
                                   cons_pointer env ) {
    struct cons_pointer result = NIL;
    struct cons_space_object arg0 = pointer2cell( frame->arg[0] );
    struct cons_space_object arg1 = pointer2cell( frame->arg[1] );
    if ( integerp( frame->arg[0] )
         && integerp( frame->arg[1] ) ) {
        result =
            make_integer( arg0.payload.integer.value
                          - arg1.payload.integer.value );
    } else if ( realp( frame->arg[0] )
                && realp( frame->arg[1] ) ) {
        result =
            make_real( arg0.payload.real.value - arg1.payload.real.value );
    } else if ( integerp( frame->arg[0] )
                && realp( frame->arg[1] ) ) {
        result =
            make_real( numeric_value
                       ( frame->arg[0] ) - arg1.payload.real.value );
    } else if ( realp( frame->arg[0] )
                && integerp( frame->arg[1] ) ) {
        result =
            make_real( arg0.payload.real.value -
                       numeric_value( frame->arg[1] ) );
    } else {
        /* TODO: throw an exception */
        lisp_throw
            ( c_string_to_lisp_string
              ( "Cannot subtract: not a number" ), frame );
    }

    // and if not nilp[frame->arg[2]) we also have an error.

    return result;
}

/**
 * Divide one number by another.
 * @param env the evaluation environment - ignored;
 * @param frame the stack frame.
 * @return a pointer to an integer or real.
 */
struct cons_pointer lisp_divide( struct
                                 stack_frame
                                 *frame, struct
                                 cons_pointer env ) {
    struct cons_pointer result = NIL;
    struct cons_space_object arg0 = pointer2cell( frame->arg[0] );
    struct cons_space_object arg1 = pointer2cell( frame->arg[1] );
    if ( numberp( frame->arg[1] )
         && numeric_value( frame->arg[1] ) == 0 ) {
        lisp_throw
            ( c_string_to_lisp_string
              ( "Cannot divide: divisor is zero" ), frame );
    } else if ( numberp( frame->arg[0] )
                && numberp( frame->arg[1] ) ) {
        long int i = ( long int )
            numeric_value( frame->arg[0] ) / numeric_value( frame->arg[1] );
        long double r = ( long double )
            numeric_value( frame->arg[0] )
            / numeric_value( frame->arg[1] );
        if ( fabsl( ( long double ) i - r ) < 0.0000000001 ) {
            result = make_integer( i );
        } else {
            result = make_real( r );
        }
    } else {
        lisp_throw
            ( c_string_to_lisp_string
              ( "Cannot divide: not a number" ), frame );
    }

    return result;
}