// JSON LISP - https://www.merrickchristensen.com/articles/json-lisp/
import { expect } from 'https://deno.land/x/expect/mod.ts'
type Json = 
  | null
  | boolean
  | number
  | string
  | Json[]
  | { [prop: string]: Json };
type Environment = {
  [name: string]: JSON | Function
};
const add             = (...args) => args.reduce((x,y) => x + y)
const subtract        = (...args) => 
  (args.length === 1 ? [0, args[0]] : args).reduce((x,y) => x - y)
const division        = (...args) => 
  (args.length === 1 ? [1, args[0]] : args).reduce((x,y) => x / y)
const multiplication  = (...args) => args.reduce((x,y) => x * y, 1)
// We provide the environment that references read from here.
const defaultEnvironment = {
  "+": add,
  "-": subtract,
  "/": division,
  "*": multiplication,
  "uppercase": (str) => str.toUpperCase(),
}
////////////////////////////////////////
export const parse = (source: string): Json => JSON.parse(source)
export const evaluate = (
  expression: Json,
  environment: Environment
) => {
  // When we encounter an expression `[ ]`
  if(Array.isArray(expression)){
    const procedure = expression[0]
    
    switch(procedure){
        // Check if we have a special form!
        case "if": {
            // Retrieve the predicate
            const predicate = expression[1]
            // Evaluate the predicate in the environment
            if(evaluate(predicate, environment)){
                // If it is true, evaluate the first branch
                return evaluate(expression[2], environment)
            } else {
                // If it is false, evaluate the false branch, if one is provided.
                if(expression[3]){
                    return evaluate(expression[3], environment)
                } else {
                    return null;
                }
            }
        }
        default: {
            // Evaluate each of the sub-expressions
            const result = expression.map((expression) => evaluate(expression, environment))
    
            // If there is nothing to apply, we have a value.
            if(result.length === 1){
                return result[0]
            } else {
                // Retrieve the procedure from the environment
                const procedure = result[0]
                // Apply it with the evaluated arguments
                return procedure(...result.slice(1))
            }
        }
    }
  } else {
    // Look up strings in the environment as references.
    if(typeof expression === "string" &&
      environment.hasOwnProperty(expression)
    ) {
      return environment[expression];
    }
    // Return values.
    return expression;
  }
}
////////////////////////////////////////////////////
expect(evaluate(10, defaultEnvironment)).toEqual(10)
expect(evaluate(["+", 5, 3, 4], defaultEnvironment)).toEqual(12)
expect(evaluate(["-", 9, 1], defaultEnvironment)).toEqual(8)
expect(evaluate(["/", 6, 2], defaultEnvironment)).toEqual(3)
expect(evaluate(["+", ["*", 2, 4], ["-", 4, 6]], defaultEnvironment)).toEqual(6)
expect(evaluate(["uppercase", "Hello world!"], defaultEnvironment)).toEqual("HELLO WORLD!")
expect(evaluate(["if", true, ["+", 1, 1]], defaultEnvironment)).toEqual(2)
expect(evaluate(["if", false, 1, ["+", 1, 2]], defaultEnvironment)).toEqual(3)
expect(evaluate(["if", false, 1], defaultEnvironment)).toEqual(null)