package nova;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.NoSuchFileException;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.format.TextStyle;
import java.util.*;
import java.util.function.BiFunction;

/**
 * Warranty & Liability
 * To the extent permitted by applicable law and unless explicitly
 * otherwise agreed upon, XLOG Technologies AG makes no warranties
 * regarding the provided information. XLOG Technologies AG assumes
 * no liability that any problems might be solved with the information
 * provided by XLOG Technologies AG.
 *
 * Rights & License
 * All industrial property rights regarding the information - copyright
 * and patent rights in particular - are the sole property of XLOG
 * Technologies AG. If the company was not the originator of some
 * excerpts, XLOG Technologies AG has at least obtained the right to
 * reproduce, change and translate the information.
 *
 * Reproduction is restricted to the whole unaltered document. Reproduction
 * of the information is only allowed for non-commercial uses. Selling,
 * giving away or letting of the execution of the library is prohibited.
 * The library can be distributed as part of your applications and libraries
 * for execution provided this comment remains unchanged.
 *
 * Restrictions
 * Only to be distributed with programs that add significant and primary
 * functionality to the library. Not to be distributed with additional
 * software intended to replace any components of the library.
 *
 * Trademarks
 * Jekejeke is a registered trademark of XLOG Technologies AG.
 */
public final class theatre {

    /**
     * https://www.baeldung.com/java-sneaky-throws
     */
    static <E extends Throwable> void sneaky(Throwable x) throws E {
        throw (E) x;
    }

    /**************************************************************/
    /* Core API                                                   */
    /**************************************************************/

    /**
     * Initialize the Novacore Prolog system subset.
     *
     * @param args The program args.
     * @throws IOException Shit happens.
     * @throws URISyntaxException Shith appens.
     */
    public static void core(String[] args) throws IOException, URISyntaxException {
        special.set_args(args);

        special.boot();
        runtime.boot();
        eval.boot();
        boot();
        Bootload.boot9();
        Bootload.boot11();
        Bootload.boot2();
        Bootload.boot4();
        Bootload.boot8();
        Bootload.boot10();
        Streams.boot5();
        Streams.boot6();
        Streams.boot7();

        String[] classpath = findRoots(runtime.class.getClassLoader());
        special.set_classpath(classpath);

        String bootbase = (classpath.length > 0 ? classpath[0] : "");
        runtime.set_bootbase(bootbase);
    }

    private static final String MARKER = "root_marker.properties";

    /**
     * Find the roots.
     *
     * @param loader The loader.
     * @return The roots.
     * @throws IOException Shit happens.
     */
    public static String[] findRoots(ClassLoader loader)
            throws IOException, URISyntaxException {
        ArrayList list = new ArrayList();
        Enumeration it = loader.getResources(MARKER);
        while (it.hasMoreElements()) {
            String path = it.nextElement().toString();
            path = path.substring(0, path.length() - MARKER.length());
            list.add(minifyURI(path));
        }
        String[] res = new String[list.size()];
        list.toArray(res);
        return res;
    }

    public static String minifyURI(String adr) throws URISyntaxException {
        URI uri = new URI(adr);
        if ("file".equals(uri.getScheme()))
            return new File(uri).toString() + File.separatorChar;
        return adr;
    }

    /**************************************************************/
    /* Main API                                                   */
    /**************************************************************/

    /**
     * Post a message to the Prolog interpreter.
     *
     * @param message The message.
     */
    public static void post(Object message) {
        Machine.register_signal(Machine.CTX_MAIN, message);
        Machine.invoke_interrupt(Machine.CTX_MAIN);
    }

    /**
     * Run a Prolog term once. The goal is run with auto-yield
     * disabled and promises are not accepted.
     *
     * @param goal The Prolog term.
     * @return boolean True or false.
     */
    public static Object perform(Object goal) {
        goal = new Store.Compound(".", new Object[]{goal, "[]"});
        return Machine.launch(goal, Machine.CTX_MAIN, Machine.VOID_ARGS);
    }

    /**
     * Run a Prolog term once. The goal is run with auto-yield
     * enabled and promises are accepted.
     *
     * @param goal The Prolog term.
     * @return boolean True or false.
     */
    public static boolean perform_async(Object goal) {
        goal = new Store.Compound(".", new Object[]{goal, "[]"});
        return Machine.launch_async(goal, Machine.CTX_MAIN, Machine.VOID_ARGS);
    }

    /**************************************************************/
    /* Register API                                                */
    /**************************************************************/

    /**
     * The flag indicates a non-void dispatch foreign function.
     */
    public static int FFI_FUNC = 0x00000100;

    /**
     * Add a Java function to the knowledge base. The given
     * function is invoked with rather slow argument copying. Also
     * the function is wrapped and registered as a special, therefore
     * not eligible to neck optimization.
     *
     * @param functor The functor.
     * @param arity   The arity.
     * @param func    The Java function.
     * @param flags   The flags.
     */
    public static void register(String functor, int arity, Handler.Builtin func, int flags) {
        Store.Provable res;
        if ((flags & FFI_FUNC) != 0) {
            res = special.make_check((args) -> invoke_func(func, args));
        } else {
            res = special.make_check((args) -> invoke(func, args));
        }
        Store.add(functor, arity, res);
    }

    private static boolean invoke(Handler.Builtin func, Object[] args) {
        Object[] temp = new Object[args.length];
        for (int i = 0; i < temp.length; i++)
            temp[i] = Machine.exec_build(args[i]);
        func.run(temp);
        return true;
    }

    private static boolean invoke_func(Handler.Builtin func, Object[] args) {
        Object[] temp = new Object[args.length - 1];
        for (int i = 0; i < temp.length; i++)
            temp[i] = Machine.exec_build(args[i]);
        Object res = func.run(temp);
        return Machine.exec_unify(args[args.length - 1], res);
    }

    /*****************************************************************/
    /* ir_object_new/1 and ir_object_current/3                       */
    /*****************************************************************/

    /**
     * ir_object_new(O):
     * The predicate succeeds in O with a new Java object.
     */
    private static boolean test_ir_object_new(Object[] args) {
        Map res = new HashMap();
        return Machine.exec_unify(args[0], res);
    }

    /**
     * ir_object_current(O, K, V):
     * The predicate succeeds in V with the value of the key K in the Java object O.
     */
    private static boolean test_ir_object_current(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        Object key = Machine.exec_build(args[1]);
        special.check_atom(key);
        Object res;
        if (obj instanceof Map) {
            res = ((Map) obj).getOrDefault(key, Store.UNDEF_OBJ);
        } else {
            try {
                Field field = obj.getClass().getField((String)key);
                res = field.get(obj);
            } catch (NullPointerException | NoSuchFieldException |
                     IllegalAccessException e) {
                res = Store.UNDEF_OBJ;
            }
        }
        if (res == Store.UNDEF_OBJ)
            return false;
        return Machine.exec_unify(args[2], res);
    }

    /*****************************************************************/
    /* ir_object_set/3, ir_object_reset/2 and ir_object_keys/2       */
    /*****************************************************************/

    /**
     * ir_object_set(O, K, V):
     * The predicate sets the value of the key K in the Java object O to V.
     */
    private static boolean test_ir_object_set(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        Object key = Machine.exec_build(args[1]);
        special.check_atom(key);
        Object value = Machine.exec_build(args[2]);
        if (obj instanceof Map) {
            ((Map) obj).put(key, value);
        } else {
            try {
                Field field = obj.getClass().getField((String)key);
                field.set(obj, value);
            } catch (NullPointerException | NoSuchFieldException |
                     IllegalAccessException e) {
                throw Machine.make_error(new Store.Compound("permission_error",
                        new Object[]{"modify", "field", key}));
            }
        }
        return true;
    }

    /**
     * ir_object_reset(O, K):
     * The predicate removes the key K from the Java object O.
     */
    private static boolean test_ir_object_reset(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        Object key = Machine.exec_build(args[1]);
        special.check_atom(key);
        if (obj instanceof Map) {
            ((Map) obj).remove(key);
        } else {
            throw Machine.make_error(new Store.Compound("permission_error",
                        new Object[]{"modify", "field", key}));
        }
        return true;
    }

    /**
     * ir_object_keys(O, L):
     * The predicate succeeds in L with the keys of the Java object O.
     */
    private static boolean test_ir_object_keys(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        if (obj instanceof Map) {
            obj = ((Map) obj).keySet();
        } else {
            Field[] fields = obj.getClass().getFields();
            obj = new FieldNames(fields);
        }
        obj = set_to_list((Collection)obj);
        return Machine.exec_unify(args[1], obj);
    }

    private static class FieldNames extends AbstractList<Object> {
        private final Field[] fields;

        public FieldNames(Field[] fields) {
            this.fields = fields;
        }

        public Object get(int index) {
            return fields[index].getName();
        }

        public int size() {
            return fields.length;
        }
    }

    /**
     * Convert a native set into a Prolog list.
     *
     * @param temp The native set.
     */
    public static Object set_to_list(Collection temp) {
        Store.Structure back = null;
        Object res = null;
        if (temp != null) {
            Iterator it = temp.iterator();
            while (it.hasNext()) {
                Object value = it.next();
                Store.Structure peek = new Store.Compound(".",
                        new Object[]{value, Store.UNDEF_OBJ});
                if (back == null) {
                    res = peek;
                } else {
                    back.args[1] = peek;
                }
                back = peek;
            }
        }
        if (back == null) {
            res = "[]";
        } else {
            back.args[1] = "[]";
        }
        return res;
    }

    /******************************************************************/
    /* ir_float_value/2                                               */
    /******************************************************************/

    /**
     * ir_float_value(P, H):
     * If P is instantiated, then the predicate succeeds in H with the
     * Java float of P, otherwise in P with the Prolog float of H.
     */
    private static boolean test_ir_float_value(Object[] args) {
        Object alpha = Machine.exec_deref(args[0]);
        if (Store.is_variable(alpha) || Machine.is_pending(alpha)) {
            Object beta = Machine.exec_build(args[1]);
            special.check_number(beta);
            Object res = (Machine.is_special(beta) ? beta :
                    special.norm_float(special.narrow_float((Number) beta)));
            return Machine.exec_unify(alpha, res);
        } else {
            alpha = Machine.exec_build(alpha);
            special.check_number(alpha);
            Object res = (Machine.is_special(alpha) ? alpha :
                    Double.valueOf(special.narrow_float((Number) alpha)));
            return Machine.exec_unify(args[1], res);
        }
    }

    /**************************************************************/
    /* os_call_later/3 and timer_cancel/1                       */
    /**************************************************************/

    private static Timer timer = null;

    private static Timer getTimer() {
        if (timer == null)
            timer = new Timer(true);
        return timer;
    }

    /**
     * os_call_later(N, D, T): internal only
     * The predicate succeeds in T with a new timer. As a side
     * effect it schedules the compiled goal N to be executed
     * after D milliseconds.
     */
    private static boolean test_os_call_later(Object[] args) {
        Object goal = Machine.exec_build(args[0]);
        runtime.check_goal(goal);
        Object obj = Machine.exec_build(args[1]);
        special.check_integer(obj);
        if (eval.integer_signum((Number) obj) < 0)
            throw Machine.make_error(new Store.Compound("domain_error",
                    new Object[]{"not_less_than_zero", obj}));
        if (!runtime.is_long((Number) obj))
            throw Machine.make_error(new Store.Compound("representation_error",
                    new Object[]{"long", obj}));
        long delay = ((Number) obj).longValue();
        Object buf = Machine.ctx;
        TimerTask tid = setDelay(() ->
                Machine.callback(goal, buf, Machine.VOID_ARGS), delay);
        return Machine.exec_unify(args[2], tid);
    }

    /**
     * timer_cancel(T):
     * The predicate succeeds. As a side effect it cancels the timer T.
     */
    private static boolean test_timer_cancel(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        check_timer(obj);
        ((TimerTask) obj).cancel();
        return true;
    }

    public static void check_timer(Object beta) {
        if (!(beta instanceof TimerTask)) {
            Store.check_nonvar(beta);
            beta = Store.copy_term(beta);
            throw Machine.make_error(new Store.Compound("type_error",
                    new Object[]{"timer", beta}));
        }
    }

    /**
     * Delay a coroutine by some milliseconds.
     *
     * @param proc The coroutine.
     * @param delay The milliseconds.
     * @return The timer.
     */
    public static TimerTask setDelay(Runnable proc, long delay) {
        TimerTask tid = new Later(proc);
        Timer timer = getTimer();
        timer.schedule(tid, delay);
        return tid;
    }

    private static class Later extends TimerTask {
        Runnable proc;

        public Later(Runnable proc) {
            this.proc = proc;
        }

        public void run() {
            new Handler.Coroutine(() -> proc.run()).async();
        }
    }

    /**************************************************************/
    /* current_task/1, task_abort/2 and os_task_create/2          */
    /**************************************************************/

    /**
     * current_task(E):
     * The predicate succeeds in E with the current task.
     */
    private static boolean test_current_task(Object[] args) {
        return Machine.exec_unify(args[0], Machine.ctx);
    }

    /**
     * task_abort(E, M):
     * The predicate succeeds. As a side effect the task E gets
     * the message M signalled.
     */
    private static boolean test_task_abort(Object[] args) {
        Object buf = Machine.exec_build(args[0]);
        check_task(buf);
        Object msg = Machine.exec_build(args[1]);
        msg = Store.copy_term(msg);
        Machine.register_signal(buf, msg);
        Machine.invoke_interrupt(buf);
        return true;
    }

    public static void check_task(Object beta) {
        if (!Machine.CTX_MAIN.equals(beta) &&
                !(beta instanceof Machine.Context)) {
            Store.check_nonvar(beta);
            beta = Store.copy_term(beta);
            throw Machine.make_error(new Store.Compound("type_error",
                    new Object[]{"task", beta}));
        }
    }
    /**
     * os_task_create(N, E): internal only
     * The predicate succeeds in E with a new task for the compiled
     * goal N. The task is scheduled to execute immediately.
     */
    private static boolean test_os_task_create(Object[] args) {
        Object goal = Machine.exec_build(args[0]);
        runtime.check_goal(goal);
        Machine.Context buf = new Machine.Context();
        setDelay(() -> Machine.task_async(goal, buf, Machine.VOID_ARGS), 0);
        return Machine.exec_unify(args[1], buf);
    }

    /******************************************************************/
    /* net_open_promise/4                                        */
    /******************************************************************/

    private static HttpClient client = null;

    public static HttpClient getHttpClient() {
        if (client == null)
            client = HttpClient.newBuilder()
                    .followRedirects(HttpClient.Redirect.NORMAL)
                    .build();
        return client;
    }

    /**
     * net_open_promise(P, L, S, Q):
     * The predicate succeeds in Q with a promise for open input S
     * on path P and option list L.
     */
    private static boolean test_net_open_promise(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        special.check_atom(obj);
        String url = (String) obj;
        Map opts = (Map) Machine.exec_build(args[1]);
        runtime.Source stream = new runtime.Source();
        if (!Machine.exec_unify(args[2], stream))
            return false;
        Object buf = Machine.ctx;
        Handler.Promise prom = open_http_promise_opts(buf, stream, url, opts);
       return Machine.exec_unify(args[3], prom);
    }

    private static Handler.Promise open_http_promise_opts(Object buf,
             runtime.Source stream, String url, Map opts) {
        return new Handler.Promise(() -> {
            Thread self = Thread.currentThread();
            Machine.register_interrupt(buf, self::interrupt);
            try {
                String method;
                HttpRequest.BodyPublisher body;
                if (opts != null && opts.get("method") != null) {
                    method = opts.get("method").toString();
                } else if (opts != null && opts.get("body") != null) {
                    method = "POST";
                } else {
                    method = "GET";
                }
                if (opts != null && opts.get("body") != null) {
                    Map opts2 = (Map) opts.get("body");
                    body = HttpRequest.BodyPublishers.ofString(
                            opts2.get("text").toString(),
                            runtime.get_encoding(opts2));
                } else {
                    body = HttpRequest.BodyPublishers.noBody();
                }
                HttpRequest.Builder builder = HttpRequest.newBuilder().method(method, body);
                if (opts != null && opts.get("headers") != null) {
                    Map headers = (Map) opts.get("headers");
                    Iterator it = headers.entrySet().iterator();
                    while (it.hasNext()) {
                        Map.Entry entry = (Map.Entry)it.next();
                        builder = builder.header((String)entry.getKey(), entry.getValue().toString());
                    }
                }
                HttpClient client = getHttpClient();
                HttpResponse<InputStream> response;
                try {
                    response = client.send(builder.uri(URI.create(url)).build(),
                            HttpResponse.BodyHandlers.ofInputStream());
                } catch (IOException err) {
                    Machine.register_signal(buf, runtime.map_file_error(err, url));
                    return;
                }
                if (response.statusCode() < 200 || 399 < response.statusCode()) {
                    Machine.register_signal(buf, map_http_result(response.statusCode(), url));
                } else {
                    if (opts != null) {
                        opts.put("uri", response.uri().toString());
                        opts.put("status", Integer.valueOf(response.statusCode()));
                        Map res = new HashMap();
                        Map<String,List<String>> map = response.headers().map();
                        Iterator it = map.entrySet().iterator();
                        while (it.hasNext()) {
                            Map.Entry entry = (Map.Entry)it.next();
                            res.put(entry.getKey(), ((List)entry.getValue()).get(0));
                        }
                        opts.put("fields", res);
                    }
                    InputStream ins = response.body();
                    stream.data = new InputStreamReader(ins, runtime.get_encoding(opts));
                    stream.receive = (BiFunction<Object, runtime.Source, Handler.Promise>) runtime::file_read_promise;
                    stream.release = (BiFunction<Object, runtime.Source, Handler.Promise>) runtime::file_close_promise;
                    stream.flags |= runtime.MASK_SRC_AREAD;
                }
            } catch (InterruptedException x) {
                /* */
            } finally {
                Machine.register_interrupt(buf, () -> {
                });
            }
        });
    }

    public static Object map_http_result(int res, String url) {
        switch (res) {
            case 403: // Forbidden
                return new Store.Compound("permission_error",
                        new Object[]{"open", "source_sink", url});
            case 404: // Not Found
                return new Store.Compound("existence_error",
                        new Object[]{"source_sink", url});
            case 405: // Method Not Allowed
                return new Store.Compound("resource_error",
                        new Object[]{"illegal_method"});
            case 500: // Internal Server Error
                return new Store.Compound("resource_error",
                        new Object[]{"internal_error"});
            case 503: // Service Unavailable
                return new Store.Compound("resource_error",
                        new Object[]{"service_unavailable"});
            default:
                return new Store.Compound("resource_error",
                        new Object[]{"io_exception"});
        }
    }

    /*******************************************************************/
    /* net_prop_promise/3                                              */
    /*******************************************************************/

    private static final String HTTP_TIME = "%a, %d %b %Y %H:%M:%S GMT";

    /**
     * net_prop_promise(F, M, Q):
     * The predicate succeeds in Q with with a promise for the
     * properties M of the file F. Barks if path F doesn't exist
     * or io exception while resolving.
     */
    private static boolean test_net_prop_promise(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        special.check_atom(obj);
        String url = (String) obj;
        Map res = new HashMap();
        if (!Machine.exec_unify(args[1], res))
            return false;
        Object buf = Machine.ctx;
        Handler.Promise prom = prop_http_promise(buf, url, res);
        return Machine.exec_unify(args[2], prom);
    }

    public static Handler.Promise prop_http_promise(Object buf, String url, Map res) {
        return new Handler.Promise(() -> {
            Thread self = Thread.currentThread();
            Machine.register_interrupt(buf, self::interrupt);
            try {
                HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.noBody();
                String method = "HEAD";
                HttpRequest.Builder builder = HttpRequest.newBuilder().method(method, body);
                HttpClient client = theatre.getHttpClient();
                HttpResponse<Void> response;
                try {
                    response = client.send(builder.uri(URI.create(url)).build(),
                            HttpResponse.BodyHandlers.discarding());
                } catch (IOException err) {
                    Machine.register_signal(buf, runtime.map_file_error(err, url));
                    return;
                }
                if (response.statusCode() != 200) {
                    Machine.register_signal(buf, theatre.map_http_result(response.statusCode(), url));
                } else {
                    Optional<String> val = response.headers().firstValue("last-modified");
                    long mtime = (val.isPresent() ? sys_time_parse(
                            val.get(), HTTP_TIME, 1) : -1);

                    res.put("last_modified", special.norm_smallint(mtime));
                    res.put("absolute_path", response.uri().toString());
                    res.put("type", "regular");
                    res.put("can_write", "false");
                }
            } catch (InterruptedException x) {
                /* */
            } finally {
                Machine.register_interrupt(buf, () -> {});
            }
        });
    }

    /******************************************************************/
    /* net_open_sync/4 and net_cntrl_sync/2                      */
    /******************************************************************/

    /**
     * net_open_sync(P, M, L, S):
     * The predicate succeeds. As a side effect the stream S is
     * opened on the path P with the mode M and the option list L.
     */
    private static boolean test_net_open_sync(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        special.check_atom(obj);
        String url = (String) obj;
        throw Machine.make_error(new Store.Compound("permission_error",
            new Object[]{"access", "source_sink", url}));
    }

    /**
     * net_cntrl_sync(F, P):
     * The predicate assigns the property P to the file F.
     */
    private static boolean test_net_cntrl_sync(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        special.check_atom(obj);
        String url = (String) obj;
        throw Machine.make_error(new Store.Compound("permission_error",
                new Object[]{"access", "source_sink", url}));
    }

    /******************************************************************/
    /* sys_atom_time/4                                                */
    /******************************************************************/

    /**
     * sys_atom_time(A, F, O, T): internal only
     * If A is a variable, the built-in succeeds in A with the millisecond
     * time T formatted by the pattern F and the flag O. Otherwise the
     * built-in succeeds in T with the millisecond time parsed from A
     * by the pattern F and the flag O.
     */
    private static boolean test_sys_atom_time(Object[] args) {
        Object fmt = Machine.exec_build(args[1]);
        special.check_atom(fmt);
        Object flag = Machine.exec_build(args[2]);
        special.check_integer(flag);
        Object text = Machine.exec_deref(args[0]);
        if (Store.is_variable(text) || Machine.is_pending(text)) {
            Object obj = Machine.exec_build(args[3]);
            special.check_integer(obj);
            if (!runtime.is_long((Number) obj))
                throw Machine.make_error(new Store.Compound("representation_error",
                        new Object[]{"long", obj}));
            long tms = ((Number) obj).longValue();
            String res;
            try {
                res = sys_time_format((String) fmt, tms, ((Number) flag).intValue());
            } catch (IllegalArgumentException x) {
                throw Machine.make_error(new Store.Compound("domain_error",
                        new Object[]{"time_format", fmt}));
            }
            return Machine.exec_unify(text, res);
        } else {
            text = Machine.exec_build(text);
            special.check_atom(text);
            long tms;
            try {
                tms = sys_time_parse((String) text, (String) fmt, ((Number) flag).intValue());
            } catch (NumberFormatException x) {
                throw Machine.make_error(new Store.Compound("syntax_error",
                        new Object[]{"illegal_date"}));
            } catch (IllegalArgumentException x) {
                throw Machine.make_error(new Store.Compound("domain_error",
                        new Object[]{"time_format", fmt}));
            }
            return Machine.exec_unify(args[3], special.norm_smallint(tms));
        }
    }

    /**
     * Parse a date time string.
     *
     * @param res The date time as a string.
     * @param pat The pattern.
     * @param utc The UTC calendar flag.
     * @return The date time in UTC milliseconds.
     */
    public static long sys_time_parse(String res, String pat, int utc) {
        int year = 1900; int month = 1; int day = 1;
        int hour = 0; int minute = 0; int second = 0;
        int j = 0;
        for (int i = 0; i < pat.length(); i++) {
            char ch = pat.charAt(i);
            if (ch == '%') {
                i++;
                int k;
                switch (pat.charAt(i)) {
                    case '%':
                        if (j < res.length() && res.charAt(j) == '%') {
                            j++;
                        } else {
                            throw new NumberFormatException("illegal date");
                        }
                        break;
                    case 'a':
                        k = scan_alphanum(res, j);
                        int weekday = parse_weekday(res.substring(j, k),
                                (utc != 0 ? Locale.US : Locale.getDefault()));
                        j = k;
                        break;
                    case 'b':
                        k = scan_alphanum(res, j);
                        month = parse_month(res.substring(j, k),
                                (utc != 0 ? Locale.US : Locale.getDefault()));
                        j = k;
                        break;
                    case 'd':
                        k = scan_integer(res, j);
                        day = Integer.parseInt(res.substring(j, k));
                        j = k;
                        break;
                    case 'm':
                        k = scan_integer(res, j);
                        month = Integer.parseInt(res.substring(j, k));
                        j = k;
                        break;
                    case 'Y':
                        k = scan_integer(res, j);
                        year = Integer.parseInt(res.substring(j, k));
                        j = k;
                        break;
                    case 'H':
                        k = scan_integer(res, j);
                        hour = Integer.parseInt(res.substring(j, k));
                        j = k;
                        break;
                    case 'M':
                        k = scan_integer(res, j);
                        minute = Integer.parseInt(res.substring(j, k));
                        j = k;
                        break;
                    case 'S':
                        k = scan_integer(res, j);
                        second = Integer.parseInt(res.substring(j, k));
                        j = k;
                        break;
                    default:
                        throw new IllegalArgumentException("illegal format");
                }
            } else {
                if (j < res.length() && res.charAt(j) == ch) {
                    j++;
                } else {
                    throw new NumberFormatException("illegal date");
                }
            }
        }
        Calendar cal;
        if (utc != 0) {
            cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        } else {
            cal = Calendar.getInstance();
        }
        cal.set(year, month - 1, day, hour, minute, second);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTimeInMillis();
    }

    /**
     * Scan alphanum.
     *
     * @param res The text.
     * @param j   The old position.
     * @return The new position.
     */
    private static int scan_alphanum(String res, int j) {
        while (j < res.length() && Character.isLetterOrDigit(res.charAt(j)))
            j++;
        return j;
    }

    /**
     * Parse a week day.
     *
     * @param str    The text.
     * @param locale The locale to use, not null
     * @return The index.
     */
    private static int parse_weekday(String str, Locale locale) {
        str = str.toLowerCase();
        for (int i = 1; i < 8; i++) {
            DayOfWeek wday = DayOfWeek.of(i);
            String val = wday.getDisplayName(TextStyle.SHORT_STANDALONE, locale);
            val = val.toLowerCase();
            if (str.equals(val))
                return i;
        }
        throw new NumberFormatException("illegal date");
    }

    /**
     * Parse a month.
     *
     * @param str    The text.
     * @param locale The locale to use, not null
     * @return The index.
     */
    private static int parse_month(String str, Locale locale) {
        str = str.toLowerCase();
        for (int i = 1; i < 13; i++) {
            Month mon = Month.of(i);
            String val = mon.getDisplayName(TextStyle.SHORT_STANDALONE, locale);
            val = val.toLowerCase();
            if (str.equals(val))
                return i;
        }
        throw new NumberFormatException("illegal date");
    }

    /**
     * Scan integer.
     *
     * @param res The text.
     * @param j   The old position.
     * @return The new position.
     */
    private static int scan_integer(String res, int j) {
        while (j < res.length() && Character.isDigit(res.charAt(j)))
            j++;
        return j;
    }

    /**
     * Format a date time string.
     *
     * @param pat The format pattern.
     * @param tms The date time in UTC milliseconds.
     * @param utc The UTC calendar flag.
     * @return The date time as a string.
     */
    public static String sys_time_format(String pat, long tms, int utc) {
        Calendar cal;
        if (utc != 0) {
            cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        } else {
            cal = Calendar.getInstance();
        }
        cal.setTimeInMillis(tms);
        StringBuilder buf = new StringBuilder();
        int k = 0;
        for (int i = 0; i < pat.length(); i++) {
            char ch = pat.charAt(i);
            if (ch == '%') {
                buf.append(pat, k, i);
                i++;
                if (i < pat.length()) {
                    switch (pat.charAt(i)) {
                        case '%':
                            buf.append('%');
                            break;
                        case 'a':
                            int j = cal.get(Calendar.DAY_OF_WEEK);
                            DayOfWeek wday = DayOfWeek.of((j != 1 ? j - 1 : 7));
                            buf.append(wday.getDisplayName(TextStyle.SHORT_STANDALONE,
                                    (utc != 0 ? Locale.US : Locale.getDefault())));
                            break;
                        case 'b':
                            Month mon = Month.of(cal.get(Calendar.MONTH) + 1);
                            buf.append(mon.getDisplayName(TextStyle.SHORT_STANDALONE,
                                    (utc != 0 ? Locale.US : Locale.getDefault())));
                            break;
                        case 'd':
                            append_padded(buf, cal.get(Calendar.DAY_OF_MONTH));
                            break;
                        case 'm':
                            append_padded(buf, cal.get(Calendar.MONTH) + 1);
                            break;
                        case 'Y':
                            buf.append(cal.get(Calendar.YEAR));
                            break;
                        case 'H':
                            append_padded(buf, cal.get(Calendar.HOUR_OF_DAY));
                            break;
                        case 'M':
                            append_padded(buf, cal.get(Calendar.MINUTE));
                            break;
                        case 'S':
                            append_padded(buf, cal.get(Calendar.SECOND));
                            break;
                        default:
                            throw new IllegalArgumentException("illegal format");
                    }
                    k = i + 1;
                } else {
                    k = i;
                }
            }
        }
        buf.append(pat, k, pat.length());
        return buf.toString();
    }

    /**
     * Append an integer value zero padded.
     *
     * @param buf   The string builder.
     * @param value The integer value.
     */
    private static void append_padded(StringBuilder buf, int value) {
        if (value < 10)
            buf.append('0');
        buf.append(value);
    }

    /******************************************************************/
    /* os_get_gestalt/1 and os_get_host/1                             */
    /******************************************************************/

    /**
     * os_get_gestalt(M): Internal only
     * The built-in succeeds in M with a gestalt properties map.
     */
    private static boolean test_os_get_gestalt(Object[] args) {
        Map gestalt = new HashMap();
        String scm = System.getenv("TERM_SCHEME");
        gestalt.put("sys_color", (scm != null ? scm : "normal"));
        Locale loc = Locale.getDefault();
        gestalt.put("sys_locale", loc.toString());
        Object res = special.objects_list(special.main_args, 0, special.main_args.length);
        gestalt.put("argv", res);
        return Machine.exec_unify(args[0], gestalt);
    }

    /**
     * os_get_host(M): internal only
     * The built-in succeeds in W with a host properties map.
     */
    private static boolean test_os_get_host(Object[] args) {
        Map res = new HashMap();
        res.put("system_url", runtime.bootbase);
        res.put("foreign_ext", special.NAT_EXT);
        res.put("host_info", System.getProperty("java.vendor")+", Java "+System.getProperty("java.version"));
        res.put("mach_info", System.getProperty("os.arch")+", "+System.getProperty("os.name"));
        return Machine.exec_unify(args[0], res);
    }

    /******************************************************************/
    /* os_get_workdir/1 and os_set_workdir/1                          */
    /******************************************************************/

    /**
     * os_get_workdir(D): internal only
     * The built-in succeeds in D with the working directory.
     */
    private static boolean test_os_get_workdir(Object[] args) {
        Object url = runtime.codebase;
        return Machine.exec_unify(args[0], url);
    }

    /**
     * os_set_workdir(D): internal only
     * The built-in succeeds. As a side effect it changes the working directory to D.
     */
    private static boolean test_os_set_workdir(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        special.check_atom(obj);
        String url = (String)obj;

        File file = runtime.findWrite(runtime.codebase, url);
        try {
            if (!file.exists())
                throw new NoSuchFileException("");
            if (!file.isDirectory())
                throw new IOException("");
        } catch (IOException err) {
            throw Machine.make_error(runtime.map_file_error(err, url));
        }

        runtime.set_codebase(file.getPath() + File.separatorChar);
        return true;
    }

    /**************************************************************/
    /* dg_get_flags/1, dg_get_partition/1 and dg_set_partition/1  */
    /**************************************************************/

    /**
     * dg_get_flags(W): internal only
     * The built-in succeeds in W with the engine flags.
     */
    private static boolean test_dg_get_flags(Object[] args) {
        return Machine.exec_unify(args[0], Integer.valueOf(Store.engine.flags));
    }

    /**
     * dg_get_partition(D): internal only
     * The built-in succeeds in D with the current stage.
     */
    private static boolean test_dg_get_partition(Object[] args) {
        return Machine.exec_unify(args[0], Store.engine.partition);
    }

    /**
     * dg_set_partition(D): internal only
     * The built-in succeeds. As a side effect it changes the current partition.
     */
    private static boolean test_dg_set_partition(Object[] args) {
        Object value = Machine.exec_build(args[0]);
        special.check_atom(value);
        Store.set_partition((String) value);
        return true;
    }

    /**************************************************************/
    /* dg_clear_stage/0, dg_get_stage/1 and dg_set_stage/1        */
    /**************************************************************/

    /**
     * dg_clear_stage: internal only
     * The built-in succeeds. As a side effect it clears the current stage.
     */
    private static boolean test_dg_clear_stage(Object[] args) {
        Store.clear();
        return true;
    }

    /**
     * dg_get_stage(D): internal only
     * The built-in succeeds in D with the current stage.
     */
    private static boolean test_dg_get_stage(Object[] args) {
        return Machine.exec_unify(args[0], Integer.valueOf(Store.stage));
    }

    /**
     * dg_set_stage(D): internal only
     * The built-in succeeds. As a side effect it changes the current stage.
     */
    private static boolean test_dg_set_stage(Object[] args) {
        Object value = Machine.exec_build(args[0]);
        special.check_integer(value);
        Store.set_stage(((Number) value).intValue());
        return true;
    }

    /******************************************************************/
    /* Theatre Init                                                   */
    /******************************************************************/

    public static void boot() {
        // object specials
        Store.add("ir_object_new", 1, special.make_check(theatre::test_ir_object_new));
        Store.add("ir_object_current", 3, special.make_check(theatre::test_ir_object_current));
        Store.add("ir_object_set", 3, special.make_check(theatre::test_ir_object_set));
        Store.add("ir_object_reset", 2, special.make_check(theatre::test_ir_object_reset));
        Store.add("ir_object_keys", 2, special.make_check(theatre::test_ir_object_keys));

        // object specials II
        Store.add("ir_float_value", 2, special.make_check(theatre::test_ir_float_value));

        // system specials, coroutines
        Store.add("os_call_later", 3, special.make_check(theatre::test_os_call_later));
        Store.add("timer_cancel", 1, special.make_check(theatre::test_timer_cancel));
        Store.add("current_task", 1, special.make_check(theatre::test_current_task));
        Store.add("task_abort", 2, special.make_check(theatre::test_task_abort));
        Store.add("os_task_create", 2, special.make_check(theatre::test_os_task_create));

        // net specials, async
        Store.add("net_open_promise", 4, special.make_check(theatre::test_net_open_promise));
        Store.add("net_prop_promise", 3, special.make_check(theatre::test_net_prop_promise));

        // net specials, sync
        Store.add("net_open_sync", 4, special.make_check(theatre::test_net_open_sync));
        Store.add("net_cntrl_sync", 2, special.make_check(theatre::test_net_cntrl_sync));

        // atom specials, time
        Store.add("sys_atom_time", 4, special.make_check(theatre::test_sys_atom_time));

        // object specials, gestalt
        Store.add("os_get_gestalt", 1, special.make_check(theatre::test_os_get_gestalt));
        Store.add("os_get_host", 1, special.make_check(theatre::test_os_get_host));
        Store.add("os_get_workdir", 1, special.make_check(theatre::test_os_get_workdir));
        Store.add("os_set_workdir", 1, special.make_check(theatre::test_os_set_workdir));

        // system specials, staging, internal only
        Store.add("dg_get_flags", 1, special.make_check(theatre::test_dg_get_flags));
        Store.add("dg_get_partition", 1, special.make_check(theatre::test_dg_get_partition));
        Store.add("dg_set_partition", 1, special.make_check(theatre::test_dg_set_partition));
        Store.add("dg_clear_stage", 0, special.make_check(theatre::test_dg_clear_stage));
        Store.add("dg_get_stage", 1, special.make_check(theatre::test_dg_get_stage));
        Store.add("dg_set_stage", 1, special.make_check(theatre::test_dg_set_stage));
    }

}
