-- Rudimentary parser for making the CAS easier to use. Essentially just wraps SymbolExpression() around symbols and Integer() around integers.


local luacas = require("luacas_init")
luacas:initglobalmodule("core")
luacas:initglobalmodule("algebra")
luacas:initglobalmodule("calculus")

-- Splits a string on a seperator.
function split(str, sep)
    local t={}
    for match in string.gmatch(str, "([^".. sep .."]+)") do
            t[#t+1] = match
    end
    return t
end

-- Displays an expression. For use in the parser.
function disp(expression, inline, simple)
    if type(expression) ~= "table" then
        tex.print(tostring(expression))
    elseif expression.autosimplify then
        if inline then
            if simple then
                tex.print('$' .. expression:autosimplify():tolatex() .. '$')
            else
                tex.print('$' .. expression:tolatex() .. '$')
            end
        else
            if simple then
                tex.print('\\[' .. expression:autosimplify():tolatex() .. '\\]')
            else
                tex.print('\\[' .. expression:tolatex() .. '\\]')
            end
        end
    else
        tex.print(tostring(expression))
    end
end

-- Displays an expression. For use in the parser.
function displua(expression)
    if type(expression) ~= "table" then
        print(tostring(expression))
    elseif expression.autosimplify then
        print(expression:autosimplify():tolatex())
    else
        print(tostring(expression))
    end
end

function vars(...)
    for _, string in ipairs(table.pack(...)) do
        if string ~= "_" then
            _G[string] = SymbolExpression(string)
        end
    end
end

function clearvars()
    for index, value in pairs(_G) do
        if type(value) == "table" and value.type and value:type() == SymbolExpression then
            _G[index] = nil
        end
    end
end

function range(a, b, step)
    if not b then
      b = a
      a = Integer.one()
    end
    step = step or Integer.one()
    local f =
      step > Integer.zero() and
        function(_, lastvalue)
          local nextvalue = lastvalue + step
          if nextvalue <= b then return nextvalue end
        end or
      step < Integer.zero() and
        function(_, lastvalue)
          local nextvalue = lastvalue + step
          if nextvalue >= b then return nextvalue end
        end or
        function(_, lastvalue) return lastvalue end
    return f, nil, a - step
  end

function factor(exp,squarefrei)
    if exp:type() == Integer then
        return exp:primefactorization()
    end
    if exp:type() == PolynomialRing then
        if not squarefrei then
            return exp:factor()
        else
            if exp.ring == Integer.getring() or Rational.getring() then
                return exp:squarefreefactorization()
            end
            if exp.ring == IntegerModN.getring() then
                return exp:modularsquarefreefactorization()
            end
            return exp:factor()
        end
    end
    return exp:autosimplify():factor()
end

function expand(exp)
    return exp:autosimplify():expand()
end

function simplify(exp)
    return exp:simplify()
end

function exp(x)
    return e^x
end

function substitute(tbl,expr)
    return expr:substitute(tbl)
end

function roots(expression)
    poly,ispoly = topoly(expression)
    if ispoly then
        return poly:roots()
    end
    return RootExpression(expression)
end

function combine(expr)
    return expr:combine()
end

function Mod(f,n)
    if f:type() == Integer then
        return IntegerModN(f,n)
    end
    if f:type() == PolynomialRing and f.ring == Integer.getring() then
        local coeffs = {}
        for i=0,f.degree:asnumber() do
            coeffs[i] = IntegerModN(f.coefficients[i],n)
        end
        return PolynomialRing(coeffs,f.symbol,f.degree)
    end
end

function Poly(coefficients,symbol,degree)
    local variable = symbol or 'x'
    return PolynomialRing:new(coefficients,variable,degree)
end

function topoly(a)
    a = a:expand():autosimplify()
    return a:topolynomial()
end

function gcd(a,b)
    if a:type() == Integer and b:type() == Integer then
        return Integer.gcd(a,b)
    end
    if a:type() == PolynomialRing and b:type() == PolynomialRing then
        return PolynomialRing.gcd(a,b)
    end
end

function gcdext(a,b)
    if a:type() == Integer and b:type() == Integer then
        return Integer.extendedgcd(a,b)
    end
    A, ATF = topoly(a)
    B, BTF = topoly(b)
    if ATF and BTF then
        return PolynomialRing.extendedgcd(A,B)
    end
    return nil,nil,nil
end

function parfrac(f,g,ffactor)
    local f,check1 = topoly(f)
    local g,check2 = topoly(g)
    if check1 and check2 then
        if f.degree >= g.degree then
            local q,r
            q,r = f:divremainder(g)
            return q + PolynomialRing.partialfractions(r,g,ffactor)
        else
            return PolynomialRing.partialfractions(f,g,ffactor)
        end
    else
        return f/g
    end
end

function factorial(a)
    return FactorialExpression(a)
end

-- Constants for the CAS. We may not want these in Lua itself, but in the latex end the user probably expects them.
e = E
pi = PI
-- sqrt = SQRT
ln = LN
log = LOG
int = INT
sin = SIN
cos = COS
tan = TAN
csc = CSC
sec = SEC
cot = COT
arcsin = ARCSIN
arccos = ARCCOS
arctan = ARCTAN
arccsc = ARCCSC
arcsec = ARCSEC
arccot = ARCCOT
abs = ABS

function ZTable(t)
    t = t or {}
    return setmetatable(t, JoinTables(getmetatable(t),
            {__index = function (t, k)
                    if type(k) == "table" and k.type and k:type() == Integer then
                        return rawget(t, k:asnumber())
                    else
                        return rawget(t, k)
                    end
                end,
             __newindex = function (t, k, v)
                    if type(k) == "table" and k.type and k:type() == Integer then
                        rawset(t, k:asnumber(), v)
                    else
                        rawset(t, k, v)
                    end
                end}))
end

function RR(n)
    if type(n) == "number" then
        return n
    end

    if type(n) == "string" then
        return tonumber(n)
    end

    if type(n) == "table" and n.asnumber then
        return n:asnumber()
    end

    error("Could not convert to a real number.")
end

function ZZ(n)
    if type(n) == "table" and n.type and n:type() == Rational then
        return n.numerator // n.denominator
    end
    return Integer(n)
end

function QQ(n)
    if type(n) == "table" then
        return n
    end

    if type(n) == "number" then
        n = tostring(n)
    end

    if type(n) == "string" then
        local parts = split(n, "%.")
        if #parts == 1 then
            return Integer(parts[1])
        else
            return Integer(parts[1])..Integer(parts[2])
        end
    end

    error("Could not convert to a rational number.")
end

--- Parses raw input into Lua code and executes it.
--- @param input string
function CASparse(input)

    -- First, we replace any occurrence of a decimal with the appropriate fraction.

    local str = string.gsub(input,"%d+%.%d+", function(s)
        local ints = split(s,"%.")
        return "("..ints[1]..ints[2].." / 10^" .. tostring(string.len(ints[2])) .. ")"
    end)

    -- Next, we replace any occurance of a number with an integer or rational version of itself.

    str = string.gsub(str, ".?[0-9]+", function (s)
        -- Here, we are part of an identifier, so we don't replace anything
        if string.match(string.sub(s, 1, 1), "[A-Z]") or string.match(string.sub(s, 1, 1), "[a-z]") or string.match(string.sub(s, 1, 1), "_") then
            return
        end

        if string.match(string.sub(s, 1, 1), "[0-9]") then
            return "Integer('" .. s .. "')"
        end

        return string.sub(s, 1, 1) .. "Integer('" .. string.sub(s, 2, #s) .. "')"
    end)

    --------------------------
    -- HERE COMES THE JANK. --
    --------------------------
    -- The JANK may not actually be doing anything now. But for the sake of posterity...
    -- Replaces each instance of a decimal with .., so we can use integer metatables to convert it into a rational properly.
    str = string.gsub(str, "Integer%('[0-9]+'%)%.Integer%('[0-9]+'%)", function (s)
        local ints = split(s, "%.")
        return "("..ints[1] .. ".." .. ints[2] .. ")"
    end)
    str = string.gsub(str, ".?%.Integer%('[0-9]+'%)", function (s)
        if string.sub(s, 1, 2) == ".." then
            return
        end
        return string.sub(s, 1, 1) .. "Integer('0')." .. string.sub(s, 2, #s)
    end)

    local exe, err = load(str .. "\n return true")
    if exe then
        exe()
    else
        print(err)
    end
end
