/*
 * Copyright (c) 2002-2016, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * http://www.opensource.org/licenses/bsd-license.php
 */
package jline;

import java.lang.reflect.Constructor;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

import jline.internal.Configuration;
import jline.internal.Log;
import static jline.internal.Preconditions.checkNotNull;

/**
 * Creates terminal instances.
 *
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @since 2.0
 */
public class TerminalFactory
{
    public static final String JLINE_TERMINAL = "jline.terminal";

    public static final String AUTO = "auto";

    public static final String UNIX = "unix";

    public static final String OSV = "osv";

    public static final String WIN = "win";

    public static final String WINDOWS = "windows";

    public static final String FREEBSD = "freebsd";

    public static final String NONE = "none";

    public static final String OFF = "off";

    public static final String FALSE = "false";

    private static Terminal term = null;

    public static synchronized Terminal create() {
    	return create(null);
    }

    public static synchronized Terminal create(String ttyDevice) {
        if (Log.TRACE) {
            //noinspection ThrowableInstanceNeverThrown
            Log.trace(new Throwable("CREATE MARKER"));
        }

        String defaultType = "dumb".equals(System.getenv("TERM")) ? NONE : AUTO;
        String type  = Configuration.getString(JLINE_TERMINAL, defaultType);

        Log.debug("Creating terminal; type=", type);

        Terminal t;
        try {
            String tmp = type.toLowerCase();

            if (tmp.equals(UNIX)) {
                t = getFlavor(Flavor.UNIX);
            }
            else if (tmp.equals(OSV)) {
                t = getFlavor(Flavor.OSV);
            }
            else if (tmp.equals(WIN) || tmp.equals(WINDOWS)) {
                t = getFlavor(Flavor.WINDOWS);
            }
            else if (tmp.equals(NONE) || tmp.equals(OFF) || tmp.equals(FALSE)) {
                if (System.getenv("INSIDE_EMACS") != null) {
                    // emacs requires ansi on and echo off
                    t = new UnsupportedTerminal(true, false);
                } else  {
                    // others the other way round
                    t = new UnsupportedTerminal(false, true);
                }
            }
            else {
                if (tmp.equals(AUTO)) {
                    String os = Configuration.getOsName();
                    Flavor flavor = Flavor.UNIX;
                    if (os.contains(WINDOWS)) {
                        flavor = Flavor.WINDOWS;
                    } else if (System.getenv("OSV_CPUS") != null) {
                        flavor = Flavor.OSV;
                    }
                    t = getFlavor(flavor, ttyDevice);
                }
                else {
                    try {
                        t = (Terminal) Thread.currentThread().getContextClassLoader().loadClass(type).newInstance();
                    }
                    catch (Exception e) {
                        throw new IllegalArgumentException(MessageFormat.format("Invalid terminal type: {0}", type), e);
                    }
                }
            }
        }
        catch (Exception e) {
            Log.error("Failed to construct terminal; falling back to unsupported", e);
            t = new UnsupportedTerminal();
        }

        Log.debug("Created Terminal: ", t);

        try {
            t.init();
        }
        catch (Throwable e) {
            Log.error("Terminal initialization failed; falling back to unsupported", e);
            return new UnsupportedTerminal();
        }

        return t;
    }

    public static synchronized void reset() {
        term = null;
    }

    public static synchronized void resetIf(final Terminal t) {
        if(t == term) {
            reset();
        }
    }

    public static enum Type
    {
        AUTO,
        WINDOWS,
        UNIX,
        OSV,
        NONE
    }

    public static synchronized void configure(final String type) {
        checkNotNull(type);
        System.setProperty(JLINE_TERMINAL, type);
    }

    public static synchronized void configure(final Type type) {
        checkNotNull(type);
        configure(type.name().toLowerCase());
    }

    //
    // Flavor Support
    //

    public static enum Flavor
    {
        WINDOWS,
        UNIX,
        OSV
    }

    private static final Map<Flavor, Class<? extends Terminal>> FLAVORS = new HashMap<Flavor, Class<? extends Terminal>>();

    static {
        registerFlavor(Flavor.WINDOWS, AnsiWindowsTerminal.class);
        registerFlavor(Flavor.UNIX, UnixTerminal.class);
        registerFlavor(Flavor.OSV, OSvTerminal.class);
    }

    public static synchronized Terminal get(String ttyDevice) {
    	// The code is assuming we've got only one terminal per process.
    	// Continuing this assumption, if this terminal is already initialized,
    	// we don't check if it's using the same tty line either. Both assumptions
    	// are a bit crude. TODO: check single terminal assumption.
        if (term == null) {
            term = create(ttyDevice);
        }
        return term;
    }

    public static synchronized Terminal get() {
        return get(null);
    }

    public static Terminal getFlavor(final Flavor flavor) throws Exception {
    	return getFlavor(flavor, null);
    }

    public static Terminal getFlavor(final Flavor flavor, String ttyDevice) throws Exception {
        Class<? extends Terminal> type = FLAVORS.get(flavor);
        Terminal result = null;
        if (type != null) {
        	if (ttyDevice != null) {
        		Constructor<?> ttyDeviceConstructor = type.getConstructor(String.class);
        		if (ttyDeviceConstructor != null) {
        			result = (Terminal) ttyDeviceConstructor.newInstance(ttyDevice);
        		} else {
        			result = type.newInstance();
        		}
        	} else {
                result = type.newInstance();
        	}
        } else {
            throw new InternalError();
        }
        return result;
    }

    public static void registerFlavor(final Flavor flavor, final Class<? extends Terminal> type) {
        FLAVORS.put(flavor, type);
    }

}
