package org.raku.nqp.sixmodel;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.util.HashMap;
import java.util.List;

import org.raku.nqp.runtime.CallFrame;
import org.raku.nqp.runtime.CallSiteDescriptor;
import org.raku.nqp.runtime.CodeRef;
import org.raku.nqp.runtime.CompilationUnit;
import org.raku.nqp.runtime.ExceptionHandling;
import org.raku.nqp.runtime.Ops;
import org.raku.nqp.runtime.ThreadContext;
import org.raku.nqp.sixmodel.reprs.KnowHOWAttributeInstance;
import org.raku.nqp.sixmodel.reprs.KnowHOWREPR;
import org.raku.nqp.sixmodel.reprs.KnowHOWREPRInstance;

/**
 * This class contains methods that belong on the KnowHOW meta-object. It
 * pretends to be a compilation unit, so as to fit with the expected API
 * for code reference like things.
 */
public class KnowHOWMethods extends CompilationUnit {
    public void new_type(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        /* Get arguments. */
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 1, 1);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            String repr_arg = Ops.namedparam_opt_s(cf, csd, args, "repr");
            String name_arg = Ops.namedparam_opt_s(cf, csd, args, "name");
            if (Ops.isnull(self) == 1 || !(self.st.REPR instanceof KnowHOWREPR))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object with REPR KnowHOWREPR");

            /* We first create a new HOW instance. */
            SixModelObject HOW = self.st.REPR.allocate(tc, self.st);

            /* See if we have a representation name; if not default to P6opaque. */
            String repr_name = repr_arg != null ? repr_arg : "P6opaque";

            /* Create a new type object of the desired REPR. (Note that we can't
             * default to KnowHOWREPR here, since it doesn't know how to actually
             * store attributes, it's just for bootstrapping knowhow's. */
            REPR repr_to_use = REPRRegistry.getByName(repr_name);
            SixModelObject type_object = repr_to_use.type_object_for(tc, HOW);

            /* See if we were given a name; put it into the meta-object if so. */
            if (name_arg != null)
                ((KnowHOWREPRInstance)HOW).name = name_arg;

            /* Set .WHO to an empty hash. */
            SixModelObject Hash = tc.gc.BOOTHash;
            type_object.st.WHO = Hash.st.REPR.allocate(tc, Hash.st);

            /* Return the type object. */
            Ops.return_o(type_object, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void add_method(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 4, 4);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            String name = Ops.posparam_s(cf, csd, args, 2);
            SixModelObject method = Ops.posparam_o(cf, csd, args, 3);

            if (Ops.isnull(self) == 1 || !(self instanceof KnowHOWREPRInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object instance with REPR KnowHOWREPR");

            ((KnowHOWREPRInstance)self).methods.put(name, method);

            Ops.return_o(method, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void add_attribute(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 3, 3);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            SixModelObject attribute = Ops.posparam_o(cf, csd, args, 2);

            if (Ops.isnull(self) == 1 || !(self instanceof KnowHOWREPRInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object instance with REPR KnowHOWREPR");
            if (Ops.isnull(attribute) == 1 || !(attribute instanceof KnowHOWAttributeInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW attributes must use KnowHOWAttributeREPR");

            ((KnowHOWREPRInstance)self).attributes.add(attribute);

            Ops.return_o(attribute, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void compose(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 2, 2);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            SixModelObject type_obj = Ops.posparam_o(cf, csd, args, 1);

            if (Ops.isnull(self) == 1 || !(self instanceof KnowHOWREPRInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object instance with REPR KnowHOWREPR");

            /* Set method cache. */
            type_obj.st.MethodCache = ((KnowHOWREPRInstance)self).methods;
            type_obj.st.ModeFlags = STable.METHOD_CACHE_AUTHORITATIVE;

            /* Set type check cache. */
            type_obj.st.TypeCheckCache = new SixModelObject[] { type_obj };

            /* Use any attribute information to produce attribute protocol
             * data. The protocol consists of an array... */
            SixModelObject repr_info = tc.gc.BOOTArray.st.REPR.allocate(tc, tc.gc.BOOTArray.st);

            /* ...which contains an array per MRO entry... */
            SixModelObject type_info = tc.gc.BOOTArray.st.REPR.allocate(tc, tc.gc.BOOTArray.st);
            repr_info.push_boxed(tc, type_info);

            /* ...which in turn contains this type... */
            type_info.push_boxed(tc, type_obj);

            /* ...then an array of hashes per attribute... */
            SixModelObject attr_info_list = tc.gc.BOOTArray.st.REPR.allocate(tc, tc.gc.BOOTArray.st);
            type_info.push_boxed(tc, attr_info_list);
            List<SixModelObject> attributes = ((KnowHOWREPRInstance)self).attributes;
            for (int i = 0; i < attributes.size(); i++) {
                KnowHOWAttributeInstance attribute = (KnowHOWAttributeInstance)attributes.get(i);
                SixModelObject attr_info = tc.gc.BOOTHash.st.REPR.allocate(tc, tc.gc.BOOTHash.st);
                SixModelObject name_obj = tc.gc.BOOTStr.st.REPR.allocate(tc, tc.gc.BOOTStr.st);
                name_obj.set_str(tc, attribute.name);
                attr_info.bind_key_boxed(tc, "name", name_obj);
                attr_info.bind_key_boxed(tc, "type", attribute.type);
                if (attribute.box_target != 0) {
                    /* Merely having the key serves as a "yes". */
                    attr_info.bind_key_boxed(tc, "box_target", attr_info);
                }
                attr_info_list.push_boxed(tc, attr_info);
            }

            /* ...followed by a list of parents (none). */
            SixModelObject parent_info = tc.gc.BOOTArray.st.REPR.allocate(tc, tc.gc.BOOTArray.st);
            type_info.push_boxed(tc, parent_info);

            /* All of this goes in a hash. */
            SixModelObject repr_info_hash = tc.gc.BOOTHash.st.REPR.allocate(tc, tc.gc.BOOTHash.st);
            repr_info_hash.bind_key_boxed(tc, "attribute", repr_info);

            /* Compose the representation using it. */
            type_obj.st.REPR.compose(tc, type_obj.st, repr_info_hash);

            Ops.return_o(type_obj, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void attributes(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 2, 2);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);

            if (Ops.isnull(self) == 1 || !(self instanceof KnowHOWREPRInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object instance with REPR KnowHOWREPR");

            SixModelObject BOOTArray = tc.gc.BOOTArray;
            SixModelObject result = BOOTArray.st.REPR.allocate(tc, BOOTArray.st);

            List<SixModelObject> attributes = ((KnowHOWREPRInstance)self).attributes;
            for (SixModelObject attr : attributes)
                result.push_boxed(tc, attr);

            Ops.return_o(result, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void methods(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 2, 2);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);

            if (Ops.isnull(self) == 1 || !(self instanceof KnowHOWREPRInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object instance with REPR KnowHOWREPR");

            SixModelObject BOOTHash = tc.gc.BOOTHash;
            SixModelObject result = BOOTHash.st.REPR.allocate(tc, BOOTHash.st);

            HashMap<String, SixModelObject> methods = ((KnowHOWREPRInstance)self).methods;
            for (String name : methods.keySet())
                result.bind_key_boxed(tc, name, methods.get(name));

            Ops.return_o(result, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void name(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 2, 2);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);

            if (Ops.isnull(self) == 1 || !(self instanceof KnowHOWREPRInstance))
                throw ExceptionHandling.dieInternal(tc, "KnowHOW methods must be called on object instance with REPR KnowHOWREPR");

            Ops.return_s(((KnowHOWREPRInstance)self).name, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void attr_new(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            /* Process arguments. */
            csd = Ops.checkarity(cf, csd, args, 1, 1);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            String name_arg = Ops.namedparam_s(cf, csd, args, "name");
            SixModelObject type_arg = Ops.namedparam_opt_o(cf, csd, args, "type");
            long bt_arg = Ops.namedparam_opt_i(cf, csd, args, "box_target");

            /* Allocate attribute object. */
            REPR repr = REPRRegistry.getByName("KnowHOWAttribute");
            KnowHOWAttributeInstance obj = (KnowHOWAttributeInstance)repr.allocate(tc, self.st);

            /* Populate it. */
            obj.name = name_arg;
            obj.type = Ops.isnull(type_arg) == 0 ? type_arg : tc.gc.KnowHOW;
            obj.box_target = bt_arg == 0 ? 0 : 1;

            /* Return produced object. */
            Ops.return_o(obj, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void attr_compose(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 1, 1);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            Ops.return_o(self, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void attr_name(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 1, 1);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            Ops.return_s(((KnowHOWAttributeInstance)self).name, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void attr_type(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 1, 1);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            Ops.return_o(((KnowHOWAttributeInstance)self).type, cf);
        }
        finally {
            cf.leave();
        }
    }

    public void attr_box_target(ThreadContext tc, CodeRef cr, CallSiteDescriptor csd, Object[] args) {
        CallFrame cf = new CallFrame(tc, cr);
        try {
            csd = Ops.checkarity(cf, csd, args, 1, 1);
            args = tc.flatArgs;
            SixModelObject self = Ops.posparam_o(cf, csd, args, 0);
            Ops.return_i(((KnowHOWAttributeInstance)self).box_target, cf);
        }
        finally {
            cf.leave();
        }
    }

    public CodeRef[] getCodeRefs() {
        CodeRef[] refs = new CodeRef[12];
        String[] snull = null;
        long[][] hnull = new long[0][];
        MethodType mt = MethodType.methodType(void.class, ThreadContext.class,
                CodeRef.class, CallSiteDescriptor.class, Object[].class);
        Lookup l = MethodHandles.lookup();
        try {
            refs[0] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "new_type", mt).bindTo(this),
                    "new_type", "new_type", snull, snull, snull, snull, hnull, (short)0);
            refs[1] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "add_method", mt).bindTo(this),
                    "add_method", "add_method", snull, snull, snull, snull, hnull, (short)0);
            refs[2] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "add_attribute", mt).bindTo(this),
                    "add_attribute", "add_attribute", snull, snull, snull, snull, hnull, (short)0);
            refs[3] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "compose", mt).bindTo(this),
                    "compose", "compose", snull, snull, snull, snull, hnull, (short)0);
            refs[4] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "attributes", mt).bindTo(this),
                    "attributes", "attributes", snull, snull, snull, snull, hnull, (short)0);
            refs[5] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "methods", mt).bindTo(this),
                    "methods", "methods", snull, snull, snull, snull, hnull, (short)0);
            refs[6] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "name", mt).bindTo(this),
                    "name", "name", snull, snull, snull, snull, hnull, (short)0);
            refs[7] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "attr_new", mt).bindTo(this),
                    "new", "attr_new", snull, snull, snull, snull, hnull, (short)0);
            refs[8] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "attr_compose", mt).bindTo(this),
                    "compose", "attr_compose", snull, snull, snull, snull, hnull, (short)0);
            refs[9] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "attr_name", mt).bindTo(this),
                    "name", "attr_name", snull, snull, snull, snull, hnull, (short)0);
            refs[10] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "attr_type", mt).bindTo(this),
                    "type", "attr_type", snull, snull, snull, snull, hnull, (short)0);
            refs[11] = new CodeRef(this, l.findVirtual(KnowHOWMethods.class, "attr_box_target", mt).bindTo(this),
                    "box_target", "attr_box_target", snull, snull, snull, snull, hnull, (short)0);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
        return refs;
    }

    public int[] getOuterMap() {
        return new int[0];
    }

    public CallSiteDescriptor[] getCallSites() {
        return new CallSiteDescriptor[0];
    }

    public String hllName() {
        return "";
    }
}
