/*
 * 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.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import jline.internal.Configuration;
import jline.internal.InfoCmp;
import jline.internal.Log;
import jline.internal.TerminalLineSettings;

import static jline.internal.Preconditions.checkNotNull;

/**
 * Terminal that is used for unix platforms. Terminal initialization
 * is handled by issuing the <em>stty</em> command against the
 * <em>/dev/tty</em> file to disable character echoing and enable
 * character input. All known unix systems (including
 * Linux and Macintosh OS X) support the <em>stty</em>), so this
 * implementation should work for an reasonable POSIX system.
 *
 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
 * @author <a href="mailto:dwkemp@gmail.com">Dale Kemp</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @author <a href="mailto:jbonofre@apache.org">Jean-Baptiste Onofr��</a>
 * @since 2.0
 */
public class UnixTerminal
    extends TerminalSupport
    implements Terminal2
{
    private final TerminalLineSettings settings;
    private final String type;
    private String intr;
    private String lnext;
    private Set<String> bools = new HashSet<String>();
    private Map<String, Integer> ints = new HashMap<String, Integer>();
    private Map<String, String> strings = new HashMap<String, String>();

    public UnixTerminal() throws Exception {
    	this(TerminalLineSettings.DEFAULT_TTY, null);
    }
    
    public UnixTerminal(String ttyDevice) throws Exception {
        this(ttyDevice, null);
    }

    public UnixTerminal(String ttyDevice, String type) throws Exception {
        super(true);
        checkNotNull(ttyDevice);
        this.settings = TerminalLineSettings.getSettings(ttyDevice);
        if (type == null) {
            type = System.getenv("TERM");
        }
        this.type = type;
        parseInfoCmp();
    }

    public TerminalLineSettings getSettings() {
        return settings;
    }

    /**
     * Remove line-buffered input by invoking "stty -icanon min 1"
     * against the current terminal.
     */
    @Override
    public void init() throws Exception {
        super.init();

        setAnsiSupported(true);

        // Set the console to be character-buffered instead of line-buffered.
        // Make sure we're distinguishing carriage return from newline.
        // Allow ctrl-s keypress to be used (as forward search)
        //
        // Please note that FreeBSD does not seem to support -icrnl and thus
        // has to be handled separately. Otherwise the console will be "stuck"
        // and will neither accept input nor print anything to stdout.
        if (Configuration.getOsName().contains(TerminalFactory.FREEBSD)) {
            settings.set("-icanon min 1 -inlcr -ixon");
        } else {
            settings.set("-icanon min 1 -icrnl -inlcr -ixon");
        }
        settings.undef("dsusp");

        setEchoEnabled(false);

        parseInfoCmp();
    }

    /**
     * Restore the original terminal configuration, which can be used when
     * shutting down the console reader. The ConsoleReader cannot be
     * used after calling this method.
     */
    @Override
    public void restore() throws Exception {
        settings.restore();
        super.restore();
    }

    /**
     * Returns the value of <tt>stty columns</tt> param.
     */
    @Override
    public int getWidth() {
        int w = settings.getProperty("columns");
        return w < 1 ? DEFAULT_WIDTH : w;
    }

    /**
     * Returns the value of <tt>stty rows>/tt> param.
     */
    @Override
    public int getHeight() {
        int h = settings.getProperty("rows");
        return h < 1 ? DEFAULT_HEIGHT : h;
    }

    @Override
    public boolean hasWeirdWrap() {
        return getBooleanCapability("auto_right_margin")
                && getBooleanCapability("eat_newline_glitch");
    }

    @Override
    public synchronized void setEchoEnabled(final boolean enabled) {
        try {
            if (enabled) {
                settings.set("echo");
            }
            else {
                settings.set("-echo");
            }
            super.setEchoEnabled(enabled);
        }
        catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Log.error("Failed to ", enabled ? "enable" : "disable", " echo", e);
        }
    }

    public void disableInterruptCharacter()
    {
        try {
            intr = getSettings().getPropertyAsString("intr");
            if ("<undef>".equals(intr)) {
                intr = null;
            }
            settings.undef("intr");
        }
        catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Log.error("Failed to disable interrupt character", e);
        }
    }

    public void enableInterruptCharacter()
    {
        try {
            if (intr != null) {
                settings.set("intr", intr);
            }
        }
        catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Log.error("Failed to enable interrupt character", e);
        }
    }

    public void disableLitteralNextCharacter()
    {
        try {
            lnext = getSettings().getPropertyAsString("lnext");
            if ("<undef>".equals(lnext)) {
                lnext = null;
            }
            settings.undef("lnext");
        }
        catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Log.error("Failed to disable litteral next character", e);
        }
    }

    public void enableLitteralNextCharacter()
    {
        try {
            if (lnext != null) {
                settings.set("lnext", lnext);
            }
        }
        catch (Exception e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            Log.error("Failed to enable litteral next character", e);
        }
    }

    public boolean getBooleanCapability(String capability) {
        return bools.contains(capability);
    }

    public Integer getNumericCapability(String capability) {
        return ints.get(capability);
    }

    public String getStringCapability(String capability) {
        return strings.get(capability);
    }

    private void parseInfoCmp() {
        String capabilities = null;
        if (type != null) {
            try {
                capabilities = InfoCmp.getInfoCmp(type);
            } catch (Exception e) {
            }
        }
        if (capabilities == null) {
            capabilities = InfoCmp.getAnsiCaps();
        }
        InfoCmp.parseInfoCmp(capabilities, bools, ints, strings);
    }
}
