/*
 [The "BSD licence"]
 Copyright (c) 2009 Shaoting Cai
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions
 are met:
 1. Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
 3. The name of the author may not be used to endorse or promote products
    derived from this software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.antlr.gunit.swingui;

import org.antlr.gunit.swingui.model.*;
import org.antlr.gunit.swingui.ImageFactory;
import java.awt.*;
import java.awt.event.*;
import java.util.HashMap;
import javax.swing.*;
import javax.swing.event.*;

/**
 *
 * @author scai
 */
public class TestCaseEditController implements IController {

    private JPanel view = new JPanel();

    private JScrollPane scroll;
    private JPanel paneDetail;
    private AbstractEditorPane paneDetailInput, paneDetailOutput;
    private JToolBar toolbar;
    private JList listCases;
    private ListModel listModel ;

    public ActionListener onTestCaseNumberChange;

    /* EDITORS */
    private InputFileEditor editInputFile;
    private InputStringEditor editInputString;
    private InputMultiEditor editInputMulti;
    private OutputResultEditor editOutputResult;
    private OutputAstEditor editOutputAST;
    private OutputStdEditor editOutputStd;
    private OutputReturnEditor editOutputReturn;
    
    private JComboBox comboInputType, comboOutputType;

    /* TYPE NAME */
    private static final String IN_TYPE_STRING = "Single-line Text";
    private static final String IN_TYPE_MULTI = "Multi-line Text";
    private static final String IN_TYPE_FILE = "Disk File";
    private static final String OUT_TYPE_BOOL = "OK or Fail";
    private static final String OUT_TYPE_AST = "AST";
    private static final String OUT_TYPE_STD = "Standard Output";
    private static final String OUT_TYPE_RET = "Return Value";

    private static final String DEFAULT_IN_SCRIPT = "";
    private static final String DEFAULT_OUT_SCRIPT = "";

    private static final Object[] INPUT_TYPE =  {
        IN_TYPE_STRING, IN_TYPE_MULTI, IN_TYPE_FILE
    };

    private static final Object[] OUTPUT_TYPE = {
        OUT_TYPE_BOOL, OUT_TYPE_AST, OUT_TYPE_STD, OUT_TYPE_RET
    };

    /* SIZE */
    private static final int TEST_CASE_DETAIL_WIDTH = 300;
    private static final int TEST_EDITOR_WIDTH = 280;
    private static final int TEST_CASE_DETAIL_HEIGHT = 250;
    private static final int TEST_EDITOR_HEIGHT = 120;

    /* MODEL */
    private Rule currentRule = null;
    private TestCase currentTestCase = null;

    /* END OF MODEL*/

    private static final HashMap<Class<?>, String> TypeNameTable;
    static {
        TypeNameTable = new HashMap<Class<?>, String> ();
        TypeNameTable.put(TestCaseInputString.class, IN_TYPE_STRING);
        TypeNameTable.put(TestCaseInputMultiString.class, IN_TYPE_MULTI);
        TypeNameTable.put(TestCaseInputFile.class, IN_TYPE_FILE);

        TypeNameTable.put(TestCaseOutputResult.class, OUT_TYPE_BOOL);
        TypeNameTable.put(TestCaseOutputAST.class, OUT_TYPE_AST);
        TypeNameTable.put(TestCaseOutputStdOut.class, OUT_TYPE_STD);
        TypeNameTable.put(TestCaseOutputReturn.class, OUT_TYPE_RET);
    }

    //private WorkSpaceView owner;

    public TestCaseEditController(WorkSpaceView workspace) {
        //this.owner = workspace;
        initComponents();
    }

    public TestCaseEditController() {
        initComponents();
    }

    public void OnLoadRule(Rule rule) {
        if(rule == null) throw new IllegalArgumentException("Null");
        this.currentRule = rule;
        this.currentTestCase = null;
        this.listModel = rule;
        this.listCases.setModel(this.listModel);      
    }

    public void setCurrentTestCase(TestCase testCase) {
        if(testCase == null) throw new IllegalArgumentException("Null");
        this.listCases.setSelectedValue(testCase, true);
        this.currentTestCase = testCase;
    }

    public Rule getCurrentRule() {
        return this.currentRule;
    }
    
    private void initComponents() {

        /* CASE LIST */
        listCases = new JList();
        listCases.addListSelectionListener(new TestCaseListSelectionListener());
        listCases.setCellRenderer(listRenderer);
        listCases.setOpaque(false);
        
        scroll = new JScrollPane(listCases);
        scroll.setBorder(BorderFactory.createTitledBorder(
                BorderFactory.createEmptyBorder(), "Test Cases"));
        scroll.setOpaque(false);
        scroll.setViewportBorder(BorderFactory.createEtchedBorder());

        /* CASE DETAIL */

        editInputString = new InputStringEditor();
        editInputMulti = new InputMultiEditor();
        editInputFile = new InputFileEditor();

        editOutputResult = new OutputResultEditor();
        editOutputAST = new OutputAstEditor();
        editOutputStd = new OutputStdEditor();
        editOutputReturn = new OutputReturnEditor();
        
        paneDetail = new JPanel();
        paneDetail.setBorder(BorderFactory.createEmptyBorder());
        paneDetail.setOpaque(false);

        comboInputType = new JComboBox(INPUT_TYPE);
        comboInputType.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                OnInputTestCaseTypeChanged(comboInputType.getSelectedItem());
            }
        });
        comboOutputType = new JComboBox(OUTPUT_TYPE);
        comboOutputType.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                OnOutputTestCaseTypeChanged(comboOutputType.getSelectedItem());
            }
        });
        paneDetailInput = new InputEditorPane(comboInputType);
        paneDetailOutput = new OutputEditorPane(comboOutputType);

        BoxLayout layout = new BoxLayout(paneDetail, BoxLayout.PAGE_AXIS);
        paneDetail.setLayout(layout);
        
        paneDetail.add(this.paneDetailInput);
        paneDetail.add(this.paneDetailOutput);

        /* TOOLBAR */
        toolbar = new JToolBar("Edit TestCases", JToolBar.VERTICAL);
        toolbar.setFloatable(false);
        toolbar.add(new AddTestCaseAction());
        toolbar.add(new RemoveTestCaseAction());

        /* COMPOSITE */
        view.setLayout(new BorderLayout());
        view.setBorder(BorderFactory.createEmptyBorder());
        view.setOpaque(false);
        view.add(toolbar, BorderLayout.WEST);
        view.add(scroll, BorderLayout.CENTER);
        view.add(paneDetail, BorderLayout.EAST);
    }

    private void updateInputEditor() {
        JComponent editor = null;

        if(currentTestCase != null ) {
            ITestCaseInput input = this.currentTestCase.getInput();
            if(input instanceof TestCaseInputString) {
                this.editInputString.setText(input.getScript());
                editor = this.editInputString;
                comboInputType.setSelectedItem(IN_TYPE_STRING);
            } else if(input instanceof TestCaseInputMultiString) {
                this.editInputMulti.setText(input.getScript());
                editor = this.editInputMulti.getView();
                comboInputType.setSelectedItem(IN_TYPE_MULTI);
            } else if(input instanceof TestCaseInputFile) {
                this.editInputFile.setText(input.getScript());
                editor = this.editInputFile;
                comboInputType.setSelectedItem(IN_TYPE_FILE);
            } else {
                throw new Error("Wrong type");
            }
        }
        
        paneDetailInput.setEditor(editor);
    }

    private void updateOutputEditor() {
        JComponent editor = null;
        
        if(currentTestCase != null) {
            
            ITestCaseOutput output = this.currentTestCase.getOutput();

            if(output instanceof TestCaseOutputAST) {

                this.editOutputAST.setText(output.getScript());
                editor = this.editOutputAST.getView();
                comboOutputType.setSelectedItem(OUT_TYPE_AST);

            } else if(output instanceof TestCaseOutputResult) {

                this.editOutputResult.setValue(output.getScript());
                editor = this.editOutputResult;
                comboOutputType.setSelectedItem(OUT_TYPE_BOOL);

            } else if(output instanceof TestCaseOutputStdOut) {

                this.editOutputStd.setText(output.getScript());
                editor = this.editOutputStd.getView();
                comboOutputType.setSelectedItem(OUT_TYPE_STD);

            } else if(output instanceof TestCaseOutputReturn) {

                this.editOutputReturn.setText(output.getScript());
                editor = this.editOutputReturn.getView();
                comboOutputType.setSelectedItem(OUT_TYPE_RET);

            } else {

                throw new Error("Wrong type");
                
            }

        }
        this.paneDetailOutput.setEditor(editor);
    }

    private void OnInputTestCaseTypeChanged(Object inputTypeStr) {
        if(this.currentTestCase != null) {
            ITestCaseInput input ;
            if(inputTypeStr == IN_TYPE_STRING) {
                input = new TestCaseInputString(DEFAULT_IN_SCRIPT);
            } else if(inputTypeStr == IN_TYPE_MULTI) {
                input = new TestCaseInputMultiString(DEFAULT_IN_SCRIPT);
            } else if(inputTypeStr == IN_TYPE_FILE) {
                input = new TestCaseInputFile(DEFAULT_IN_SCRIPT);
            } else {
                throw new Error("Wrong Type");
            }

            if(input.getClass().equals(this.currentTestCase.getInput().getClass()))
                return ;

            this.currentTestCase.setInput(input);
        }
        this.updateInputEditor();
    }

    private void OnOutputTestCaseTypeChanged(Object outputTypeStr) {
        if(this.currentTestCase != null) {

            ITestCaseOutput output ;
            if(outputTypeStr == OUT_TYPE_AST) {
                output = new TestCaseOutputAST(DEFAULT_OUT_SCRIPT);
            } else if(outputTypeStr == OUT_TYPE_BOOL) {
                output = new TestCaseOutputResult(false);
            } else if(outputTypeStr == OUT_TYPE_STD) {
                output = new TestCaseOutputStdOut(DEFAULT_OUT_SCRIPT);
            } else if(outputTypeStr == OUT_TYPE_RET) {
                output = new TestCaseOutputReturn(DEFAULT_OUT_SCRIPT);
            } else {
                throw new Error("Wrong Type");
            }

            if(output.getClass().equals(this.currentTestCase.getOutput().getClass()))
                return ;

            this.currentTestCase.setOutput(output);
        }
        this.updateOutputEditor();
    }


    private void OnTestCaseSelected(TestCase testCase) {
        //if(testCase == null) throw new RuntimeException("Null TestCase");
        this.currentTestCase = testCase;
        updateInputEditor();
        updateOutputEditor();

    }

    private void OnAddTestCase() {
        if(currentRule == null) return;
        
        final TestCase newCase = new TestCase(
                new TestCaseInputString(""),
                new TestCaseOutputResult(true));
        this.currentRule.addTestCase(newCase);
        setCurrentTestCase(newCase);

        this.listCases.setSelectedValue(newCase, true);
        this.listCases.updateUI();
        this.OnTestCaseSelected(newCase);
        this.onTestCaseNumberChange.actionPerformed(null);
    }

    private void OnRemoveTestCase() {
        if(currentTestCase == null) return;
        currentRule.removeElement(currentTestCase);
        listCases.updateUI();

        final TestCase nextActiveCase = listCases.isSelectionEmpty() ?
            null : (TestCase) listCases.getSelectedValue() ;
        OnTestCaseSelected(nextActiveCase);
        this.onTestCaseNumberChange.actionPerformed(null);
    }

    public Object getModel() {
        return currentRule;
    }

    public Component getView() {
        return view;
    }

    /* EDITOR CONTAINER */

    abstract public class AbstractEditorPane extends JPanel {

        private JComboBox combo;
        private JComponent editor;
        private String title;
        private JLabel placeHolder = new JLabel();

        public AbstractEditorPane(JComboBox comboBox, String title) {
            this.combo = comboBox;
            this.editor = placeHolder;
            this.title = title;
            this.initComponents();
        }

        private void initComponents() {
            placeHolder.setPreferredSize(new Dimension(
                    TEST_CASE_DETAIL_WIDTH, TEST_CASE_DETAIL_HEIGHT));
            this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
            this.add(combo, BorderLayout.NORTH);
            this.add(editor, BorderLayout.CENTER);
            this.setOpaque(false);
            this.setBorder(BorderFactory.createTitledBorder(title));
            this.setPreferredSize(new Dimension(
                    TEST_CASE_DETAIL_WIDTH, TEST_CASE_DETAIL_HEIGHT));
        }

        public void setEditor(JComponent newEditor) {
            if(newEditor == null) newEditor = placeHolder;
            this.remove(editor);
            this.add(newEditor);
            this.editor = newEditor;
            this.updateUI();
        }
    }

    public class InputEditorPane extends AbstractEditorPane {
        public InputEditorPane(JComboBox comboBox) {
            super(comboBox, "Input");
        }
    }

    public class OutputEditorPane extends AbstractEditorPane {
        public OutputEditorPane(JComboBox comboBox) {
            super(comboBox, "Output");
        }
    }

    /* INPUT EDITORS */

    public class InputStringEditor extends JTextField implements CaretListener {
        public InputStringEditor() {
            super();

            this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
            this.addCaretListener(this);
        }

        public void caretUpdate(CaretEvent arg0) {
            currentTestCase.getInput().setScript(getText());
            listCases.updateUI();
        }
    }

    public class InputMultiEditor implements CaretListener {
        private JTextArea textArea = new JTextArea(20, 30);
        private JScrollPane scroll = new JScrollPane(textArea,
                JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

        public InputMultiEditor() {
            super();
            scroll.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
            textArea.addCaretListener(this);
        }

        public void caretUpdate(CaretEvent arg0) {
            currentTestCase.getInput().setScript(getText());
            listCases.updateUI();
        }

        public String getText() {
            return textArea.getText();
        }

        public void setText(String text) {
            textArea.setText(text);
        }

        public JComponent getView() {
            return scroll;
        }
    }

    public class InputFileEditor extends InputStringEditor {};

    public class OutputResultEditor extends JPanel implements ActionListener {
        
        private JToggleButton tbFail, tbOk;

        public OutputResultEditor() {
            super();

            tbFail = new JToggleButton("Fail");
            tbOk = new JToggleButton("OK");
            ButtonGroup group = new ButtonGroup();
            group.add(tbFail);
            group.add(tbOk);

            this.add(tbFail);
            this.add(tbOk);

            this.tbFail.addActionListener(this);
            this.tbOk.addActionListener(this);

            this.setPreferredSize(
                    new Dimension(TEST_EDITOR_WIDTH, 100));
        }

        public void actionPerformed(ActionEvent e) {
            TestCaseOutputResult output =
                    (TestCaseOutputResult) currentTestCase.getOutput();

            if(e.getSource() == tbFail) {
                output.setScript(false);
            } else {
                output.setScript(true);
            }

            listCases.updateUI();
        }

        public void setValue(String value) {
            if(TestCaseOutputResult.OK.equals(value)) {
                this.tbOk.setSelected(true);
            } else {
                this.tbFail.setSelected(true);
            }
        }
    }
    

    public class OutputAstEditor implements CaretListener {
        private JTextArea textArea = new JTextArea(20, 30);
        private JScrollPane scroll = new JScrollPane(textArea,
                JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

        public OutputAstEditor() {
            super();
            scroll.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
            textArea.addCaretListener(this);
        }

        public void caretUpdate(CaretEvent arg0) {
            currentTestCase.getOutput().setScript(getText());
            listCases.updateUI();
        }

        public void setText(String text) {
            this.textArea.setText(text);
        }

        public String getText() {
            return this.textArea.getText();
        }

        public JScrollPane getView() {
            return this.scroll;
        }
    }


    public class OutputStdEditor extends OutputAstEditor {}
    public class OutputReturnEditor extends OutputAstEditor {}

    /* EVENT HANDLERS */

    private class TestCaseListSelectionListener implements ListSelectionListener {

        public void valueChanged(ListSelectionEvent e) {
            
            if(e.getValueIsAdjusting()) return;
            final JList list = (JList) e.getSource();
            final TestCase value = (TestCase) list.getSelectedValue();
            if(value != null) OnTestCaseSelected(value);
            
        }

    }

    /* ACTIONS */

    private class AddTestCaseAction extends AbstractAction {
        public AddTestCaseAction() {
            super("Add", ImageFactory.getSingleton().ADD);
            putValue(SHORT_DESCRIPTION, "Add a gUnit test case.");
        }
        public void actionPerformed(ActionEvent e) {
            OnAddTestCase();
        }
    }

    private class RemoveTestCaseAction extends AbstractAction {
        public RemoveTestCaseAction() {
            super("Remove", ImageFactory.getSingleton().DELETE);
            putValue(SHORT_DESCRIPTION, "Remove a gUnit test case.");
        }
        public void actionPerformed(ActionEvent e) {
            OnRemoveTestCase();
        }
    }

    /* CELL RENDERERS */

    private static TestCaseListRenderer listRenderer
            = new TestCaseListRenderer();

    private static class TestCaseListRenderer implements ListCellRenderer {

        private static Font IN_FONT = new Font("mono", Font.PLAIN, 12);
        private static Font OUT_FONT = new Font("default", Font.BOLD, 12);

        public static String clamp(String text, int len) {
            if(text.length() > len) {
                return text.substring(0, len - 3).concat("...");
            } else {
                return text;
            }
        }

        public static String clampAtNewLine(String text) {
            int pos = text.indexOf('\n');
            if(pos >= 0) {
                return text.substring(0, pos).concat("...");
            } else {
                return text;
            }
        }

        public Component getListCellRendererComponent(
                JList list, Object value, int index,
                boolean isSelected, boolean hasFocus) {

            final JPanel pane = new JPanel();
            
            if (value instanceof TestCase) {
                final TestCase item = (TestCase) value;

                // create components
                final JLabel labIn = new JLabel(
                        clamp(clampAtNewLine(item.getInput().getScript()), 18));
                final JLabel labOut = new JLabel(
                        clamp(clampAtNewLine(item.getOutput().getScript()), 18));
                labOut.setFont(OUT_FONT);
                labIn.setFont(IN_FONT);

                labIn.setIcon(item.getInput() instanceof TestCaseInputFile ?
                    ImageFactory.getSingleton().FILE16 :
                    ImageFactory.getSingleton().EDIT16);

                pane.setBorder(BorderFactory.createEtchedBorder());
                pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
                pane.add(labIn);
                pane.add(labOut);
                pane.setBackground(isSelected ? Color.LIGHT_GRAY : Color.WHITE);
            } 

            return pane;
        }
    }

}
