/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2018 Osimis S.A., Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


package com.orthancserver;

import org.json.simple.JSONValue;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Collections;
import java.util.Enumeration;


import java.awt.Canvas;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingWorker;
import javax.swing.border.EmptyBorder;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;


public class SelectImageDialog extends JDialog 
{
	private enum ResourceType {
		ROOT,
      SERVER,
			PATIENT,
			STUDY,
			SERIES,
			INSTANCE,
      INFO;
	}


	private static class MyTreeNode extends DefaultMutableTreeNode implements Comparable 
  {
    private OrthancConnection orthanc_;
		private boolean loaded_ = false;
		private ResourceType type_;
		private String uuid_;


    public String GetUuid()
    {
      return uuid_;
    }

    public ResourceType GetResourceType()
    {
      return type_;
    }

    public OrthancConnection GetConnection()
    {
      return orthanc_;
    }

    public boolean UpdatePreview(PreviewPanel preview)
    {
      if (type_ == ResourceType.INSTANCE)
      {
        preview.Load(orthanc_, "/instances/" + uuid_ + "/preview");
        return true;
      }
      else if (type_ == ResourceType.SERIES)
      {
        try
        {
          JSONObject series = (JSONObject) orthanc_.ReadJson("series/" + uuid_);
          JSONArray instances = (JSONArray) series.get("Instances");
          if (instances.size() > 0)
          {
            preview.Load(orthanc_, "/instances/" + instances.get(0) + "/preview");
            return true;
          }
        }
        catch (IOException e)
        {
        }
      }

      preview.Reset();
      return false;
    }


    private String AddComponent(String source, String component)
    {
      if (component == null ||
          component.length() == 0)
      {
        return source;
      }

      if (source.length() == 0)
      {
        return component;
      }
      else
      {
        return source + " - " + component;
      }
    }


    private List<MyTreeNode> LoadPatients() throws IOException
    {
      List<MyTreeNode> children = new ArrayList<MyTreeNode>();

      JSONArray patients = (JSONArray) orthanc_.ReadJson("patients");
      for (int i = 0; i < patients.size(); i++)
      {
        String uuid = (String) patients.get(i);
        JSONObject patient = (JSONObject) orthanc_.ReadJson("patients/" + uuid);
        JSONObject main = (JSONObject) patient.get("MainDicomTags");

        String s = new String();
        s = AddComponent(s, (String) main.get("PatientID"));
        s = AddComponent(s, (String) main.get("PatientName"));
        children.add(new MyTreeNode(orthanc_, ResourceType.PATIENT, uuid, s));
      }      

      return children;
    }


    private List<MyTreeNode> LoadStudies() throws IOException
    {
      List<MyTreeNode> children = new ArrayList<MyTreeNode>();

      JSONObject patient = (JSONObject) orthanc_.ReadJson("patients/" + uuid_);
      JSONArray studies = (JSONArray) patient.get("Studies");
      for (int i = 0; i < studies.size(); i++)
      {
        String uuid = (String) studies.get(i);
        JSONObject study = (JSONObject) orthanc_.ReadJson("studies/" + uuid);
        JSONObject main = (JSONObject) study.get("MainDicomTags");

        String s = new String();
        s = AddComponent(s, (String) main.get("StudyDescription"));
        s = AddComponent(s, (String) main.get("StudyDate"));
        children.add(new MyTreeNode(orthanc_, ResourceType.STUDY, uuid, s));
      }

      return children;
    }


    private List<MyTreeNode> LoadSeries() throws IOException
    {
      List<MyTreeNode> children = new ArrayList<MyTreeNode>();

      JSONObject study = (JSONObject) orthanc_.ReadJson("studies/" + uuid_);
      JSONArray seriesSet = (JSONArray) study.get("Series");
      for (int i = 0; i < seriesSet.size(); i++)
      {
        String uuid = (String) seriesSet.get(i);
        JSONObject series = (JSONObject) orthanc_.ReadJson("series/" + uuid);
        JSONObject main = (JSONObject) series.get("MainDicomTags");

        String s = new String();
        s = AddComponent(s, (String) main.get("Modality"));
        s = AddComponent(s, (String) main.get("SeriesDescription"));
        children.add(new MyTreeNode(orthanc_, ResourceType.SERIES, uuid, s));
      }

      return children;
    }


    private List<MyTreeNode> LoadInstances() throws IOException
    {
      List<MyTreeNode> children = new ArrayList<MyTreeNode>();

      JSONObject series = (JSONObject) orthanc_.ReadJson("series/" + uuid_);
      JSONArray instances = (JSONArray) series.get("Instances");
      for (int i = 0; i < instances.size(); i++)
      {
        String uuid = (String) instances.get(i);
        JSONObject instance = (JSONObject) orthanc_.ReadJson("instances/" + uuid);
        Long index = (Long) instance.get("IndexInSeries");
        String s;
        if (index == null)
        {
          s = uuid;
        }
        else
        {
          s = String.valueOf(index);
        }

        children.add(new MyTreeNode(orthanc_, ResourceType.INSTANCE, uuid, s));
      }

      return children;
    }


    public MyTreeNode()  // Create root node
    {
      loaded_ = true;
      orthanc_ = null;
      type_ = ResourceType.ROOT;
      uuid_ = "";
			setAllowsChildren(true);
    }

		public MyTreeNode(OrthancConnection orthanc,
                      ResourceType type, 
                      String id, 
                      String name) 
		{
      orthanc_ = orthanc;
			type_ = type;
			uuid_ = id;
			add(new DefaultMutableTreeNode("Loading...", false));
			setAllowsChildren(true);
			setUserObject(name);
		}

		@Override
    public int compareTo(Object other) 
		{
			String a = (String) getUserObject();
			String b = (String) ((MyTreeNode) other).getUserObject();
			return a.compareTo(b);
    }

		private void SetChildren(List<MyTreeNode> children) 
		{
      Collections.sort(children);

			removeAllChildren();
			setAllowsChildren(children.size() > 0);
			for (MutableTreeNode node : children) 
			{
				add(node);
			}
			loaded_ = true;
		}

		@Override
		public boolean isLeaf() 
		{
			return type_ == ResourceType.INSTANCE || type_ == ResourceType.INFO;
		}

		public void LoadChildren(final DefaultTreeModel model) 
		{
			if (loaded_) 
			{
				return;
			}

			SwingWorker<List<MyTreeNode>, Void> worker = new SwingWorker<List<MyTreeNode>, Void>() {
				@Override
				protected List<MyTreeNode> doInBackground() 
        {
					List<MyTreeNode> children = null;

          try
          {
            switch (type_)
            {
              case SERVER:   children = LoadPatients(); break;
              case PATIENT:  children = LoadStudies(); break;
              case STUDY:    children = LoadSeries(); break;
              case SERIES:   children = LoadInstances(); break;
              default:
                break;
            }            
          }
          catch (IOException e)
          {
            children = null;
          }

          if (children == null)
          {
            children = new ArrayList<MyTreeNode>();
            children.add(new MyTreeNode(orthanc_, ResourceType.INFO, "", "Orthanc is not running, or bad credentials"));
          }

					return children;
				}

				@Override
				protected void done() 
        {
					try 
          {
						SetChildren(get());
						model.nodeStructureChanged(MyTreeNode.this);
					}
          catch (Exception e) 
          {
					}

					super.done();
				}
			};

			worker.execute();
		}

	}


  private MyTreeNode root_ = new MyTreeNode();
  private JTree tree_ = null;
  private boolean isSuccess_;
  private String selectedUuid_;
  private ResourceType selectedType_;
  private OrthancConnection selectedConnection_;
  private JButton okButton_ = new JButton("Open");
  private JButton removeServer_ = new JButton("Remove server");
  private PreviewPanel preview_ = new PreviewPanel();

  public SelectImageDialog()
  {
    tree_ = new JTree();

    tree_.addTreeWillExpandListener(new TreeWillExpandListener() 
    {
      @Override
      public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
        TreePath path = event.getPath();
        if (path.getLastPathComponent() instanceof MyTreeNode) {
          MyTreeNode node = (MyTreeNode) path.getLastPathComponent();
          node.LoadChildren((DefaultTreeModel) tree_.getModel());
        }
      }

      @Override
      public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
      }
    });


    tree_.addTreeSelectionListener(new TreeSelectionListener()
    {
      @Override
      public void valueChanged(TreeSelectionEvent e) {
        TreePath path = e.getNewLeadSelectionPath();
        if (path != null) 
        {
          MyTreeNode node = (MyTreeNode) path.getLastPathComponent();
          if (node.UpdatePreview(preview_))
          {
            selectedType_ = node.GetResourceType();
            selectedUuid_ = node.GetUuid();
            selectedConnection_ = node.GetConnection();
            okButton_.setEnabled(true);
          }

          removeServer_.setEnabled(node.GetResourceType() == ResourceType.SERVER);
        }
      }
    });


    tree_.addMouseListener(new MouseAdapter() 
    {
      public void mousePressed(MouseEvent e) {
        TreePath path = tree_.getPathForLocation(e.getX(), e.getY());
        if (path != null) {
          MyTreeNode node = (MyTreeNode) path.getLastPathComponent();
          if (e.getClickCount() == 2 &&
              node.GetResourceType() == ResourceType.INSTANCE) {
            // Double click on an instance, close the dialog
            isSuccess_ = true;
            setVisible(false);
          }
        }
      }
    });


    final JPanel contentPanel = new JPanel();
		getContentPane().setLayout(new BorderLayout());
		contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
		getContentPane().add(contentPanel, BorderLayout.CENTER);
		contentPanel.setLayout(new BorderLayout(0, 0));
		{
			JSplitPane splitPane = new JSplitPane();
			splitPane.setResizeWeight(0.6);
			contentPanel.add(splitPane);
			
			splitPane.setLeftComponent(new JScrollPane(tree_));
			splitPane.setRightComponent(preview_);
		}
		{
			JPanel buttonPane = new JPanel();
			buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
			getContentPane().add(buttonPane, BorderLayout.SOUTH);
			{
				JButton btnAddServer = new JButton("Add server");
        btnAddServer.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent arg) {
            OrthancConfigurationDialog dd = new OrthancConfigurationDialog();
            dd.setLocationRelativeTo(null);  // Center dialog on screen

            OrthancConnection orthanc = dd.ShowModal();
            if (orthanc != null) {
              AddOrthancServer(orthanc);
              ((DefaultTreeModel) tree_.getModel()).reload();
            }
          }
        });
				buttonPane.add(btnAddServer);
			}

			{
				buttonPane.add(removeServer_);
        removeServer_.setEnabled(false);

        removeServer_.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent arg) {
            MyTreeNode selected = (MyTreeNode) tree_.getLastSelectedPathComponent(); 
            if (selected.GetResourceType() == ResourceType.SERVER &&
                JOptionPane.showConfirmDialog(null, "Remove server \"" + selected.getUserObject() + "\"?",
                                              "WARNING", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
            {
              ((DefaultTreeModel) tree_.getModel()).removeNodeFromParent(selected);
            }
          }
        });
			}

			{
        okButton_.setEnabled(false);
				okButton_.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent arg) {
            isSuccess_ = true;
            setVisible(false);
          }
        });
				buttonPane.add(okButton_);
				getRootPane().setDefaultButton(okButton_);
			}
			{
				JButton cancelButton = new JButton("Cancel");
				cancelButton.addActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent arg) {
            setVisible(false);
					}
				});
				buttonPane.add(cancelButton);
			}
		}

    setUndecorated(false);
    setSize(500,500);
    setTitle("Select some series or some instance in Orthanc");
    setModal(true);
  }


  public void AddOrthancServer(OrthancConnection orthanc)
  {
    root_.add(new MyTreeNode(orthanc, ResourceType.SERVER, "", orthanc.GetName()));
  }

  public boolean ShowModal()
  {
    final DefaultTreeModel model = new DefaultTreeModel(root_);
    root_.LoadChildren(model);
    tree_.setRootVisible(false);
    tree_.setShowsRootHandles(true);
    tree_.setModel(model);

    isSuccess_ = false;
    setVisible(true);
    return isSuccess_;
  }

  public String GetSelectedUuid()
  {
    return selectedUuid_;
  }

  public boolean IsSeriesSelected()
  {
    return selectedType_ == ResourceType.SERIES;
  }

  public boolean IsInstanceSelected()
  {
    return selectedType_ == ResourceType.INSTANCE;
  }

  public OrthancConnection GetSelectedConnection()
  {
    return selectedConnection_;
  }

  public void Select(OrthancConnection c, boolean isInstance, String uuid)  // For test
  {
    selectedConnection_ = c;
    selectedType_ = (isInstance ? ResourceType.INSTANCE : ResourceType.SERIES);
    selectedUuid_ = uuid;
  }

  public void Unserialize(String s)
  {
    if (s.length() == 0)
    {
      // Add default Orthanc server
      AddOrthancServer(new OrthancConnection());
    }
    else
    {
      // https://stackoverflow.com/a/13109632/881731
      String decoded = OrthancConnection.DecodeBase64(s);
      
      JSONArray config = (JSONArray) JSONValue.parse(decoded);
      if (config != null)
      {
        for (int i = 0; i < config.size(); i++)
        {
          AddOrthancServer(OrthancConnection.Unserialize((JSONObject) config.get(i)));
        }
      }
    }
  }

  public String Serialize()
  {
    JSONArray servers = new JSONArray();

    for (int i = 0; i < root_.getChildCount(); i++)
    {
      MyTreeNode node = (MyTreeNode) root_.getChildAt(i);
      servers.add(node.GetConnection().Serialize());
    }

    String config = servers.toJSONString();

    // https://stackoverflow.com/a/13109632/881731
    return new String(Base64.getEncoder().encode(config.getBytes()));
  }
}
