#
# mal (Make a Lisp) object types
#

if [ -z "${__mal_types_included__}" ]; then
__mal_types_included=true

declare -A ANON

__obj_magic=__5bal7
__keyw=$(echo -en "\xCA\x9E") # \u029E
__obj_hash_code=${__obj_hash_code:-0}

__new_obj_hash_code () {
    __obj_hash_code=$(( __obj_hash_code + 1))
    r="${__obj_hash_code}"
}

__new_obj () {
    __new_obj_hash_code
    r="${1}_${r}"
}

__new_obj_like () {
    __new_obj_hash_code
    r="${1%_*}_${r}"
}


# Errors/Exceptions

__ERROR=
_error() {
    _string "${1}"
    __ERROR="${r}"
    r=
}



#
# General functions
#

# Return the type of the object (or "make" if it's not a object
_obj_type () {
    local type="${1:0:4}"
    r=
    case "${type}" in
        symb) r="symbol" ;;
        list) r="list" ;;
        numb) r="number" ;;
        func) r="function" ;;
        strn)
            local s="${ANON["${1}"]}"
            if [[ "${1:0:1}" = "${__keyw}" ]] \
                || [[ "${1:0:2}" = "${__keyw}" ]]; then
                r="keyword"
            else
                r="string"
            fi ;;
        _nil) r="nil" ;;
        true) r="true" ;;
        fals) r="false" ;;
        vect) r="vector" ;;
        hmap) r="hash_map" ;;
        atom) r="atom" ;;
        undf) r="undefined" ;;
        *)    r="bash" ;;
    esac
}

_equal? () {
    _obj_type "${1}"; local ot1="${r}"
    _obj_type "${2}"; local ot2="${r}"
    if [[ "${ot1}" != "${ot2}" ]]; then
        if ! _sequential? "${1}" || ! _sequential? "${2}"; then
            return 1
        fi
    fi
    case "${ot1}" in
    string|symbol|keyword|number)
        [[ "${ANON["${1}"]}" == "${ANON["${2}"]}" ]] ;;
    list|vector)
        _count "${1}"; local sz1="${r}"
        _count "${2}"; local sz2="${r}"
        [[ "${sz1}" == "${sz2}" ]] || return 1
        local a1=(${ANON["${1}"]})
        local a2=(${ANON["${2}"]})
        for ((i=0;i<${#a1[*]};i++)); do
            _equal? "${a1[${i}]}" "${a2[${i}]}" || return 1
        done
        ;;
    hash_map)
        local hm1="${ANON["${1}"]}"
        eval local ks1="\${!${hm1}[@]}"
        local hm2="${ANON["${2}"]}"
        eval local ks2="\${!${hm2}[@]}"
        [[ "${#ks1}" == "${#ks2}" ]] || return 1
        for k in ${ks1}; do
            eval v1="\${${hm1}[\"${k}\"]}"
            eval v2="\${${hm2}[\"${k}\"]}"
            [ "${v1}" ] || return 1
            [ "${v2}" ] || return 1
            _equal? "${v1}" "${v2}" || return 1
        done
        ;;
    *)
        [[ "${1}" == "${2}" ]] ;;
    esac
}

# Constant atomic values

__nil=_nil_0
__true=true_0
__false=fals_0

_nil? () { [[ ${1} =~ ^_nil_ ]]; }
_true? () { [[ ${1} =~ ^true_ ]]; }
_false? () { [[ ${1} =~ ^fals_ ]]; }


# Symbols

_symbol () {
    __new_obj_hash_code
    r="symb_${r}"
    ANON["${r}"]="${1//\*/__STAR__}"
}
_symbol? () { [[ ${1} =~ ^symb_ ]]; }


# Keywords

_keyword () {
    local k="${1}"
    __new_obj_hash_code
    r="strn_${r}"
    if [[ "${1:0:1}" = "${__keyw}" ]] \
        || [[ "${1:0:2}" = "${__keyw}" ]]; then
        true
    else
        k="${__keyw}${1}"
    fi
    ANON["${r}"]="${k//\*/__STAR__}"
}
_keyword? () {
    [[ ${1} =~ ^strn_ ]] || return 1
    local s="${ANON["${1}"]}"
    [[ "${s:0:1}" = "${__keyw}" ]] || [[ "${s:0:2}" = "${__keyw}" ]]
}


# Numbers

_number () {
    __new_obj_hash_code
    r="numb_${r}"
    ANON["${r}"]="${1}"
}
_number? () { [[ ${1} =~ ^numb_ ]]; }


# Strings

_string () {
    __new_obj_hash_code
    r="strn_${r}"
    ANON["${r}"]="${1//\*/__STAR__}"
}
_string? () { [[ ${1} =~ ^strn_ ]]; }


# Functions
# Return a function object. The first parameter is the
# function 'source'.
_function () {
    __new_obj_hash_code
    eval "function ${__obj_magic}_func_${r} () { ${1%;} ; }"
    r="func_${r}"
    if [[ "${2}" ]]; then
        # Native function
        ANON["${r}"]="${__obj_magic}_${r}@${2}@${3}@${4}"
    else
        # Bash function
        ANON["${r}"]="${__obj_magic}_${r}"
    fi
}
_function? () { [[ ${1} =~ ^func_ ]]; }


# Lists

_list () {
    __new_obj_hash_code
    r="list_${r}"
    ANON["${r}"]="${*}"
}
_list? () { [[ ${1} =~ ^list_ ]]; }


# Vectors

_vector () {
    __new_obj_hash_code
    r="vector_${r}"
    ANON["${r}"]="${*}"
}
_vector? () { [[ ${1} =~ ^vector_ ]]; }

vec () {
    __new_obj_hash_code
    r="vector_$r"
    ANON["$r"]=${ANON["$1"]}
}


# hash maps (associative arrays)

_hash_map () {
    __new_obj_hash_code
    local name="hmap_${r}"
    local obj="${__obj_magic}_${name}"
    declare -A -g ${obj}; eval "${obj}=()"
    ANON["${name}"]="${obj}"

    while [[ "${1}" ]]; do
        eval ${obj}[\"${ANON["${1}"]}\"]=\"${2}\"
        shift; shift
    done

    r="${name}"
}
_hash_map? () { [[ ${1} =~ ^hmap_ ]]; }

_contains? () {
    local obj="${ANON["${1}"]}"
    eval [[ "\${${obj}[\"${2}\"]+isset}" ]]
}

_copy_hash_map () {
    local orig_obj="${ANON["${1}"]}"
    _hash_map
    local name="${r}"
    local obj="${ANON["${name}"]}"

    # Copy the existing key/values to the new object
    local temp=$(typeset -p ${orig_obj})
    eval ${temp/#declare -A ${orig_obj}=/declare -A -g ${obj}=}
    r="${name}"
}

# Return same hash map with keys/values added/mutated in place
_assoc! () {
    local obj=${ANON["${1}"]}; shift
    declare -A -g ${obj}

    # Set the key/values specified
    while [[ "${1}" ]]; do
        eval ${obj}[\"${1}\"]=\"${2}\"
        shift; shift
    done
}

# Return same hash map with keys/values deleted/mutated in place
_dissoc! () {
    local obj=${ANON["${1}"]}; shift
    declare -A -g ${obj}

    # Delete the key/values specified
    while [[ "${1}" ]]; do
        eval unset ${obj}[\"${1}\"]
        shift
    done
}


# Atoms

_atom() {
    __new_obj_hash_code
    r="atom_${r}"
    ANON["${r}"]="${*}"
}
_atom? () { [[ ${1} =~ ^atom_ ]]; }


# sequence operations

_sequential? () {
    _list? "${1}" || _vector? "${1}"
}

_nth () {
    local temp=(${ANON["${1}"]})
    r="${temp[${2}]}"
}

_first () {
    local temp="${ANON["${1}"]}"
    r="${temp%% *}"
    [ "${r}" ] || r="${__nil}"
}

_last () {
    local temp="${ANON["${1}"]}"
    r="${temp##* }"
}

# Creates a new vector/list of the everything after but the first
# element
_rest () {
    local temp="${ANON["${1}"]}"
    _list
    if [[ "${temp#* }" == "${temp}" ]]; then
        ANON["${r}"]=
    else
        ANON["${r}"]="${temp#* }"
    fi
}


_empty? () { [[ -z "${ANON["${1}"]}" ]]; }

# conj that mutates in place (and always appends)
_conj! () {
    local obj="${1}"; shift
    local obj_data="${ANON["${obj}"]}"
    ANON["${obj}"]="${obj_data:+${obj_data} }${*}"
    r="${1}"
}



_count () {
    if _nil? "${1}"; then
        r="0"
    else
        local temp=(${ANON["${1}"]})
        r=${#temp[*]}
    fi
}

# Slice a sequence object $1 starting at $2 of length $3
_slice () {
    local temp=(${ANON["${1}"]})
    __new_obj_like "${1}"
    ANON["${r}"]="${temp[@]:${2}:${3}}"
}

# Takes a bash function and an list object and invokes the function on
# each element of the list, returning a new list (or vector) of the results.
_map_with_type () {
    local constructor="${1}"; shift
    local f="${1}"; shift
    local items="${ANON["${1}"]}"; shift
    eval "${constructor}"; local new_seq="${r}"
    for v in ${items}; do
        #echo eval ${f%%@*} "${v}" "${@}"
        eval ${f%%@*} "${v}" "${@}"
        [[ "${__ERROR}" ]] && r= && return 1
        _conj! "${new_seq}" "${r}"
    done
    r="${new_seq}"
}

_map () {
    _map_with_type _list "${@}"
}

fi
