/*
 * Copyright (c) 1998-2019 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.ui.op;

import java.nio.charset.StandardCharsets;
import org.bounce.text.LineNumberMargin;
import org.bounce.text.ScrollableEditorPanel;
import org.bounce.text.xml.XMLEditorKit;
import org.bounce.text.xml.XMLStyleConstants;
import org.jdom2.Element;
import ucar.nc2.NetcdfFileWriter.Version;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.NetcdfDatasets;
import ucar.nc2.ncml.NcMLReader;
import ucar.nc2.ui.ToolsUI;
import ucar.nc2.ui.dialog.NetcdfOutputChooser;
import ucar.nc2.write.NcmlWriter;
import ucar.nc2.write.NetcdfFormatWriter;
import ucar.ui.widget.BAMutil;
import ucar.ui.widget.FileManager;
import ucar.ui.widget.IndependentWindow;
import ucar.ui.widget.TextHistoryPane;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.IO;
import ucar.nc2.write.Nc4ChunkingStrategy;
import ucar.util.prefs.PreferencesExt;
import ucar.ui.prefs.ComboBox;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.text.PlainDocument;

/**
 * An Editor for NcML files.
 *
 * @author caron
 * @since 3/13/13
 */
public class NcmlEditor extends JPanel {
  private static final boolean debugNcmlWrite = false;

  private NetcdfDataset ds;
  private String ncmlLocation;
  private JEditorPane editor;
  private Map<String, String> protoMap = new HashMap<>(10);
  private ComboBox<String> protoChooser;

  private TextHistoryPane infoTA;
  private IndependentWindow infoWindow;
  private FileManager fileChooser;
  private NetcdfOutputChooser outChooser;

  private final AbstractButton coordButt;
  private boolean addCoords;

  private PreferencesExt prefs;


  public NcmlEditor(JPanel buttPanel, PreferencesExt prefs) {
    this.prefs = prefs;
    fileChooser = new FileManager(null, null, null, (PreferencesExt) prefs.node("FileManager"));

    AbstractAction coordAction = new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        addCoords = (Boolean) getValue(BAMutil.STATE);
        String tooltip = addCoords ? "add Coordinates is ON" : "add Coordinates is OFF";
        coordButt.setToolTipText(tooltip);
      }
    };
    addCoords = prefs.getBoolean("coordState", false);
    String tooltip2 = addCoords ? "add Coordinates is ON" : "add Coordinates is OFF";
    BAMutil.setActionProperties(coordAction, "nj22/AddCoords", tooltip2, true, 'C', -1);
    coordAction.putValue(BAMutil.STATE, addCoords);
    coordButt = BAMutil.addActionToContainer(buttPanel, coordAction);

    protoChooser = new ComboBox<>((PreferencesExt) prefs.node("protoChooser"));
    addProtoChoices();
    buttPanel.add(protoChooser);
    protoChooser.addActionListener(e -> {
      String ptype = (String) protoChooser.getSelectedItem();
      String proto = protoMap.get(ptype);
      if (proto != null) {
        editor.setText(proto);
      }
    });

    editor = new JEditorPane();

    // Instantiate a XMLEditorKit with wrapping enabled.
    XMLEditorKit kit = new XMLEditorKit();

    // Enable auto indentation.
    kit.setAutoIndentation(true);

    // Enable tag completion.
    kit.setTagCompletion(true);

    // Set a style
    kit.setStyle(XMLStyleConstants.ATTRIBUTE_NAME, Color.RED, Font.BOLD);

    editor.setEditorKit(kit);

    // Set the font style.
    editor.setFont(new Font("Monospaced", Font.PLAIN, 12));

    // Set the tab size
    editor.getDocument().putProperty(PlainDocument.tabSizeAttribute, 2);

    // Put the editor in a panel that will force it to resize, when a different view is choosen.
    ScrollableEditorPanel editorPanel = new ScrollableEditorPanel(editor);

    JScrollPane scroller = new JScrollPane(editorPanel);

    // Add the number margin as a Row Header View
    scroller.setRowHeaderView(new LineNumberMargin(editor));

    AbstractAction saveAction = new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        String location = (ds == null) ? ncmlLocation : ds.getLocation();
        if (location == null) {
          location = "test";
        }

        int pos = location.lastIndexOf(".");
        if (pos > 0) {
          location = location.substring(0, pos);
        }

        String filename = fileChooser.chooseFilenameToSave(location + ".ncml");
        if (filename == null) {
          return;
        }
        if (doSaveNcml(editor.getText(), filename)) {
          ncmlLocation = filename;
        }
      }
    };
    BAMutil.setActionProperties(saveAction, "Save", "Save NcML", false, 'S', -1);
    BAMutil.addActionToContainer(buttPanel, saveAction);

    AbstractAction netcdfAction = new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        if (outChooser == null) {
          outChooser = new NetcdfOutputChooser((Frame) null);
          outChooser.addPropertyChangeListener("OK", new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent evt) {
              writeNetcdf((NetcdfOutputChooser.Data) evt.getNewValue());
            }
          });
        }

        String location = (ds == null) ? ncmlLocation : ds.getLocation();
        if (location == null) {
          location = "test";
        }

        int pos = location.lastIndexOf(".");
        if (pos > 0) {
          location = location.substring(0, pos);
        }
        outChooser.setOutputFilename(location);
        outChooser.setVisible(true);
      }
    };
    BAMutil.setActionProperties(netcdfAction, "nj22/Netcdf", "Write netCDF file", false, 'N', -1);
    BAMutil.addActionToContainer(buttPanel, netcdfAction);

    AbstractAction transAction = new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        doTransform(editor.getText());
      }
    };
    BAMutil.setActionProperties(transAction, "Import",
        "read textArea through NcMLReader\n write NcML back out via resulting dataset", false, 'T', -1);
    BAMutil.addActionToContainer(buttPanel, transAction);

    AbstractButton compareButton = BAMutil.makeButtcon("Select", "Check NcML", false);
    compareButton.addActionListener(e -> {
      Formatter f = new Formatter();
      checkNcml(f);

      infoTA.setText(f.toString());
      infoTA.gotoTop();
      infoWindow.show();
    });
    buttPanel.add(compareButton);

    setLayout(new BorderLayout());
    add(scroller, BorderLayout.CENTER);

    // the info window
    infoTA = new TextHistoryPane();
    infoWindow = new IndependentWindow("Extra Information", BAMutil.getImage("nj22/NetcdfUI"), infoTA);
    infoWindow.setBounds((Rectangle) prefs.getBean("InfoWindowBounds", new Rectangle(300, 300, 500, 300)));
  }

  public void save() {
    fileChooser.save();
    prefs.putBeanObject("InfoWindowBounds", infoWindow.getBounds());
  }

  public void closeOpenFiles() {
    try {
      if (ds != null) {
        ds.close();
      }
    } catch (IOException ioe) {
      // Nothing to do here.
    }
    ds = null;
  }

  public boolean setNcml(String cmd) {
    if (cmd.endsWith(".xml") || cmd.endsWith(".ncml")) {
      if (!cmd.startsWith("http:") && !cmd.startsWith("file:")) {
        cmd = "file:" + cmd;
      }
      ncmlLocation = cmd;
      String text = IO.readURLcontents(cmd);
      editor.setText(text);
    } else {
      writeNcml(cmd);
    }
    return true;
  }

  /**
   * write ncml from given dataset
   */
  boolean writeNcml(String location) {
    boolean err = false;

    closeOpenFiles();

    try {
      String result;
      ds = openDataset(location, addCoords, null);
      if (ds == null) {
        editor.setText("Failed to open <" + location + ">");
      } else {
        NcmlWriter ncmlWriter = new NcmlWriter();
        Element netcdfElem = ncmlWriter.makeNetcdfElement(ds, null);
        result = ncmlWriter.writeToString(netcdfElem);

        editor.setText(result);
        editor.setCaretPosition(0);
      }
    } catch (Exception e) {
      StringWriter sw = new StringWriter(10000);
      e.printStackTrace();
      e.printStackTrace(new PrintWriter(sw));
      editor.setText(sw.toString());
      err = true;
    }

    return !err;
  }

  private NetcdfDataset openDataset(String location, boolean addCoords, CancelTask task) {
    try {
      boolean useBuilders = ToolsUI.getToolsUI().getUseBuilders();
      return useBuilders ? NetcdfDatasets.openDataset(location, addCoords, task)
          : NetcdfDataset.openDataset(location, addCoords, task);

      // if (setUseRecordStructure)
      // ncd.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);
    } catch (IOException ioe) {
      JOptionPane.showMessageDialog(null, "NetcdfDatasets.open cannot open " + ioe.getMessage());
      if (!(ioe instanceof FileNotFoundException)) {
        ioe.printStackTrace();
      }
      return null;
    }
  }

  void writeNetcdf(NetcdfOutputChooser.Data data) {
    String text = editor.getText();

    try {
      ByteArrayInputStream bis = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
      Version version = NetcdfFormatWriter.convertToNetcdfFileWriterVersion(data.format);
      NcMLReader.writeNcMLToFile(bis, data.outputFilename, version,
          Nc4ChunkingStrategy.factory(data.chunkerType, data.deflate, data.shuffle));
      JOptionPane.showMessageDialog(this, "File successfully written");

    } catch (Exception exc) {
      JOptionPane.showMessageDialog(this, "ERROR: " + exc.getMessage());
      exc.printStackTrace();
    }
  }

  /**
   * Read text from textArea through NcMLReader
   * then write it back out via resulting dataset
   */
  void doTransform(String text) {
    try (StringReader reader = new StringReader(text);
        NetcdfDataset ncd = NcMLReader.readNcML(reader, null);
        StringWriter sw = new StringWriter(10000)) {
      ncd.writeNcML(sw, null);
      editor.setText(sw.toString());
      editor.setCaretPosition(0);
      JOptionPane.showMessageDialog(this, "File successfully transformed");
    } catch (IOException ioe) {
      JOptionPane.showMessageDialog(this, "ERROR: " + ioe.getMessage());
      ioe.printStackTrace();
    }
  }

  /**
   * Read text from textArea through NcMLReader
   * then write it back out via resulting dataset
   */
  private void checkNcml(Formatter f) {
    if (ncmlLocation == null) {
      return;
    }
    boolean useBuilders = ToolsUI.getToolsUI().getUseBuilders();
    try (NetcdfDataset ncd =
        useBuilders ? NetcdfDatasets.openDataset(ncmlLocation) : NetcdfDataset.openDataset(ncmlLocation)) {
      ncd.check(f);
    } catch (IOException ioe) {
      JOptionPane.showMessageDialog(this, "ERROR: " + ioe.getMessage());
      ioe.printStackTrace();
    }
  }

  boolean doSaveNcml(String text, String filename) {
    if (debugNcmlWrite) {
      System.out.println("filename=" + filename);
      System.out.println("text=" + text);
    }

    File out = new File(filename);
    if (out.exists()) {
      int val = JOptionPane.showConfirmDialog(null, filename + " already exists. Do you want to overwrite?", "WARNING",
          JOptionPane.YES_NO_OPTION);
      if (val != JOptionPane.YES_OPTION) {
        return false;
      }
    }

    try {
      IO.writeToFile(text, out);
      JOptionPane.showMessageDialog(this, "File successfully written");
      return true;
    } catch (IOException ioe) {
      JOptionPane.showMessageDialog(this, "ERROR: " + ioe.getMessage());
      ioe.printStackTrace();
      return false;
    }
    // saveNcmlDialog.setVisible(false);
  }

  void addProtoChoices() {
    String xml = "<?xml version='1.0' encoding='UTF-8'?>\n"
        + "<netcdf xmlns='http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2'>\n"
        + "  <variable name='time' type='int' shape='time'>\n"
        + "    <attribute name='long_name' type='string' value='time coordinate' />\n"
        + "    <attribute name='units' type='string' value='days since 2001-8-31 00:00:00 UTC' />\n"
        + "    <values start='0' increment='10' />\n" + "  </variable>\n"
        + "  <aggregation dimName='time' type='joinNew'>\n" + "    <variableAgg name='T'/>\n"
        + "    <scan location='src/test/data/ncml/nc/' suffix='.nc' subdirs='false'/>\n" + "  </aggregation>\n"
        + "</netcdf>";
    protoMap.put("joinNew", xml);
    protoChooser.addItem("joinNew");

    xml = "<?xml version='1.0' encoding='UTF-8'?>\n"
        + "<netcdf xmlns='http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2'>\n"
        + "  <aggregation dimName='time' type='joinExisting'>\n"
        + "    <scan location='ncml/nc/pfeg/' suffix='.nc' />\n" + "  </aggregation>\n" + "</netcdf>";
    protoMap.put("joinExisting", xml);
    protoChooser.addItem("joinExisting");
  }
}