/*
 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.internal.jshell.tool;

import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static jdk.internal.jshell.tool.JShellTool.RECORD_SEPARATOR;
import static jdk.internal.jshell.tool.JShellTool.getResource;
import static jdk.internal.jshell.tool.JShellTool.readResource;
import static jdk.internal.jshell.tool.JShellTool.toPathResolvingUserHome;

/**
 * Processing start-up "script" information.  The startup may consist of several
 * entries, each of which may have been read from a user file or be a built-in
 * resource.  The startup may also be empty ("-none"); Which is stored as the
 * empty string different from unset (null).  Built-in resources come from
 * resource files.  Startup is stored as named elements rather than concatenated
 * text, for display purposes but also importantly so that when resources update
 * with new releases the built-in will update.
 * @author Robert Field
 */
class Startup {

    // Store one entry in the start-up list
    private static class StartupEntry {

        // is this a JShell built-in?
        private final boolean isBuiltIn;

        // the file or resource name
        private final String name;

        // the commands/snippets as text
        private final String content;

        // for files, the date/time read in -- makes clear it is a snapshot
        private final String timeStamp;

        StartupEntry(boolean isBuiltIn, String name, String content) {
            this(isBuiltIn, name, content, "");
        }

        StartupEntry(boolean isBuiltIn, String name, String content, String timeStamp) {
            this.isBuiltIn = isBuiltIn;
            this.name = name;
            this.content = content;
            this.timeStamp = timeStamp;
        }

        // string form to store in storage (e.g. Preferences)
        String storedForm() {
            return (isBuiltIn ? "*" : "-") + RECORD_SEPARATOR +
                    name + RECORD_SEPARATOR +
                    timeStamp + RECORD_SEPARATOR +
                    content + RECORD_SEPARATOR;
        }

        // the content
        @Override
        public String toString() {
            return content;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 41 * hash + (this.isBuiltIn ? 1 : 0);
            hash = 41 * hash + Objects.hashCode(this.name);
            if (!isBuiltIn) {
                hash = 41 * hash + Objects.hashCode(this.content);
            }
            return hash;
        }

        // built-ins match on name only.  Time stamp isn't considered
        @Override
        public boolean equals(Object o) {
            if (!(o instanceof StartupEntry)) {
                return false;
            }
            StartupEntry sue = (StartupEntry) o;
            return isBuiltIn == sue.isBuiltIn &&
                     name.equals(sue.name) &&
                     (isBuiltIn || content.equals(sue.content));
        }
    }

    private static final String DEFAULT_STARTUP_NAME = "DEFAULT";

    // cached DEFAULT start-up
    private static Startup defaultStartup = null;

    // the list of entries
    private List<StartupEntry> entries;

    // the concatenated content of the list of entries
    private String content;

    // created only with factory methods (below)
    private Startup(List<StartupEntry> entries) {
        this.entries = entries;
        this.content = entries.stream()
                .map(sue -> sue.toString())
                .collect(joining());
    }

    private Startup(StartupEntry entry) {
        this(Collections.singletonList(entry));
    }

    // retrieve the content
    @Override
    public String toString() {
        return content;
    }

    @Override
    public int hashCode() {
        return 9  + Objects.hashCode(this.entries);
    }

    @Override
    public boolean equals(Object o) {
        return (o instanceof Startup)
                && entries.equals(((Startup) o).entries);
    }

    // are there no entries ("-none")?
    boolean isEmpty() {
        return entries.isEmpty();
    }

    // is this the "-default" setting -- one entry which is DEFAULT
    boolean isDefault() {
        if (entries.size() == 1) {
            StartupEntry sue = entries.get(0);
            if (sue.isBuiltIn && sue.name.equals(DEFAULT_STARTUP_NAME)) {
                return true;
            }
        }
        return false;
    }

    // string form to store in storage (e.g. Preferences)
    String storedForm() {
        return entries.stream()
                .map(sue -> sue.storedForm())
                .collect(joining());
    }

    // show commands to re-create
    String show(boolean isRetained) {
        String cmd = "/set start " + (isRetained ? "-retain " : "");
        if (isDefault()) {
            return cmd + "-default\n";
        } else if (isEmpty()) {
            return cmd + "-none\n";
        } else {
            return entries.stream()
                    .map(sue -> sue.name)
                    .collect(joining(" ", cmd, "\n"));
        }
    }

    // show corresponding contents for show()
    String showDetail() {
        if (isDefault() || isEmpty()) {
            return "";
        } else {
            return entries.stream()
                    .map(sue -> "---- " + sue.name
                            + (sue.timeStamp.isEmpty()
                                    ? ""
                                    : " @ " + sue.timeStamp)
                            + " ----\n" + sue.content)
                    .collect(joining());
        }
    }

    /**
     * Factory method: Unpack from stored form.
     *
     * @param storedForm the Startup in the form as stored on persistent
     * storage (e.g. Preferences)
     * @param mh handler for error messages
     * @return Startup, or default startup when error (message has been printed)
     */
    static Startup unpack(String storedForm, MessageHandler mh) {
        if (storedForm != null) {
            if (storedForm.isEmpty()) {
                return noStartup();
            }
            try {
                String[] all = storedForm.split(RECORD_SEPARATOR);
                if (all.length == 1) {
                    // legacy (content only)
                    return new Startup(new StartupEntry(false, "user.jsh", storedForm));
                } else if (all.length % 4 == 0) {
                    List<StartupEntry> e = new ArrayList<>(all.length / 4);
                    for (int i = 0; i < all.length; i += 4) {
                        final boolean isBuiltIn;
                        switch (all[i]) {
                            case "*":
                                isBuiltIn = true;
                                break;
                            case "-":
                                isBuiltIn = false;
                                break;
                            default:
                                throw new IllegalArgumentException("Unexpected StartupEntry kind: " + all[i]);
                        }
                        String name = all[i + 1];
                        String timeStamp = all[i + 2];
                        String content = all[i + 3];
                        if (isBuiltIn) {
                            // update to current definition, use stored if removed/error
                            String resource = getResource(name);
                            if (resource != null) {
                                content = resource;
                            }
                        }
                        e.add(new StartupEntry(isBuiltIn, name, content, timeStamp));
                    }
                    return new Startup(e);
                } else {
                    throw new IllegalArgumentException("Unexpected StartupEntry entry count: " + all.length);
                }
            } catch (Exception ex) {
                mh.errormsg("jshell.err.corrupted.stored.startup", ex.getMessage());
            }
        }
        return defaultStartup(mh);
    }

    /**
     * Factory method: Read Startup from a list of external files or resources.
     *
     * @param fns list of file/resource names to access
     * @param context printable non-natural language context for errors
     * @param mh handler for error messages
     * @return files as Startup, or null when error (message has been printed)
     */
    static Startup fromFileList(List<String> fns, String context, MessageHandler mh) {
        List<StartupEntry> entries = fns.stream()
                .map(fn -> readFile(fn, context, mh))
                .collect(toList());
        if (entries.stream().anyMatch(sue -> sue == null)) {
            return null;
        }
        return new Startup(entries);
    }

    /**
     * Read a external file or a resource.
     *
     * @param filename file/resource to access
     * @param context printable non-natural language context for errors
     * @param mh handler for error messages
     * @return file as startup entry, or null when error (message has been printed)
     */
    private static StartupEntry readFile(String filename, String context, MessageHandler mh) {
        if (filename != null) {
            try {
                byte[] encoded = Files.readAllBytes(toPathResolvingUserHome(filename));
                return new StartupEntry(false, filename, new String(encoded),
                        LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)));
            } catch (AccessDeniedException e) {
                mh.errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage());
            } catch (NoSuchFileException e) {
                String resource = getResource(filename);
                if (resource != null) {
                    // Not found as file, but found as resource
                    return new StartupEntry(true, filename, resource);
                }
                mh.errormsg("jshell.err.file.not.found", context, filename);
            } catch (Exception e) {
                mh.errormsg("jshell.err.file.exception", context, filename, e);
            }
        } else {
            mh.errormsg("jshell.err.file.filename", context);
        }
        return null;

    }

    /**
     * Factory method: The empty Startup ("-none").
     *
     * @return the empty Startup
     */
    static Startup noStartup() {
        return new Startup(Collections.emptyList());
    }

    /**
     * Factory method: The default Startup ("-default.").
     *
     * @param mh handler for error messages
     * @return The default Startup, or empty startup when error (message has been printed)
     */
    static Startup defaultStartup(MessageHandler mh) {
        if (defaultStartup != null) {
            return defaultStartup;
        }
        try {
            String content = readResource(DEFAULT_STARTUP_NAME);
            return defaultStartup = new Startup(
                    new StartupEntry(true, DEFAULT_STARTUP_NAME, content));
        } catch (AccessDeniedException e) {
            mh.errormsg("jshell.err.file.not.accessible", "jshell", DEFAULT_STARTUP_NAME, e.getMessage());
        } catch (NoSuchFileException e) {
            mh.errormsg("jshell.err.file.not.found", "jshell", DEFAULT_STARTUP_NAME);
        } catch (Exception e) {
            mh.errormsg("jshell.err.file.exception", "jshell", DEFAULT_STARTUP_NAME, e);
        }
        return defaultStartup = noStartup();
    }

}
