/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.javafxdata.datasources.protocol;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyFloatWrapper;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.util.Callback;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.javafxdata.datasources.io.DataSourceReader;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 *
 * @author johan
 */
public class XMLDataSource<S, T> extends DataSource<Node> implements ListWrapper, TableWrapper {

  private Map<String, TableColumn<S, Object>> mappedColumns = new HashMap<String, TableColumn<S, Object>>();
  private ObservableList<TableColumn<S, ?>> tableColumns;
  private XPath xpath;
  private List<String> selectedColumnNames = new LinkedList<String>();

  public XMLDataSource(DataSourceReader reader, String tagName) throws IOException, ParserConfigurationException, SAXException {
    this(reader, tagName, new String[0]);
  }

  public XMLDataSource(DataSourceReader reader, String tagName, String... cols) throws IOException, ParserConfigurationException, SAXException {
    selectedColumnNames.addAll(Arrays.asList(cols));
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
    DataRetriever retriever = new DataRetriever(reader, tagName);
    retriever.start();
  }

  public XMLDataSource(String string, String tagName) throws ParserConfigurationException, SAXException, IOException {
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    initializeDataRows(db.parse(string), tagName);
  }

  public XMLDataSource(InputSource source, String tagName) throws ParserConfigurationException, SAXException, IOException {
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    initializeDataRows(db.parse(source), tagName);
  }

  public XMLDataSource(InputStream stream, String tagName) throws ParserConfigurationException, SAXException, IOException {
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    initializeDataRows(db.parse(stream), tagName);
  }

  public XMLDataSource(File file, String tagName) throws ParserConfigurationException, SAXException, IOException {
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    initializeDataRows(db.parse(file), tagName);
  }

  private void initializeDataRows(Document xml, String tagName) {
    xml.getDocumentElement().normalize();

    NodeList xmlNodes;
    if (tagName != null && !tagName.isEmpty()) {
      xmlNodes = xml.getElementsByTagName(tagName);
    } else {
      xmlNodes = xml.getDocumentElement().getChildNodes();
    }

    for (int i = 0; i < xmlNodes.getLength(); i++) {
      add(xmlNodes.item(i));
    }
  }

  public TableColumn<S, Object> getNamedColumn(final String name, final Class clazz) {
    TableColumn<S, Object> answer = mappedColumns.get(name);
    if (answer == null) {
      answer = new TableColumn<S, Object>(name);

      answer.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<S, Object>, ObservableValue<Object>>() {

        @Override
        public ObservableValue<Object> call(CellDataFeatures<S, Object> cdf) {
          Node myRow = (Node) cdf.getValue();
          if (clazz.equals(Long.class)) {
            return (ObservableValue) new ReadOnlyLongWrapper(Long.parseLong(getMyElement(myRow, name)));
          }
          if (clazz.equals(Float.class)) {
            return (ObservableValue) new ReadOnlyFloatWrapper(Float.parseFloat(getMyElement(myRow, name)));
          } else {
            return (ObservableValue) new ReadOnlyStringWrapper(getMyElement(myRow, name));
          }
        }
      });
    }
    return answer;
  }

  @Override
  public TableColumn<S, ?> getNamedColumn(final String name) {
    int idx = selectedColumnNames.indexOf(name);
    if (idx > -1) {
      return getColumns().get(idx);
    } else {
      return new TableColumn<S, T>();
    }
  }

  @Override
  public ObservableList<TableColumn<S, ?>> getColumns() {
    if (selectedColumnNames == null) {
      return FXCollections.emptyObservableList();
    }

    if (tableColumns != null) {
      return tableColumns;
    }

    // build columns
    tableColumns = FXCollections.observableArrayList();
    for (final String columnName : selectedColumnNames) {
      TableColumn<S, T> tc = new TableColumn<S, T>(columnName);

      tc.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<S, T>, ObservableValue<T>>() {

        @Override
        public ObservableValue<T> call(CellDataFeatures<S, T> cdf) {
          Node myRow = (Node) cdf.getValue();
          return (ObservableValue<T>) new ReadOnlyStringWrapper(getMyElement(myRow, columnName));
        }
      });

      tableColumns.add(tc);


    }
    return tableColumns;
  }

  private String getMyElement(Node node, String columnName) {
    //     System.out.println("GetMyElement asked for columnName = "+columnName+", node = "+node.getTextContent());
    if (node != null) {
      try {
        XPathExpression expression = xpath.compile(columnName);
        String e = expression.evaluate(node);
        //                    System.out.println("GetMyElement returned for columnName = "+columnName+": "+e);

        return e;
      } catch (XPathExpressionException ex) {
        Logger.getLogger(XMLDataSource.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
    return null;
  }

  class DataRetriever extends Service {

    private final DataSourceReader myReader;
    private final String myTagName;

    public DataRetriever(DataSourceReader reader, String tagName) {
      this.myReader = reader;
      this.myTagName = tagName;
    }

    @Override
    protected Task createTask() {
      Task task = new Task() {

        @Override
        protected Object call() throws Exception {
          DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
          DocumentBuilder db = dbf.newDocumentBuilder();
          initializeDataRows(db.parse(myReader.getInputStream()), myTagName);
          Document xml = db.parse(myReader.getInputStream());
          String tagName = myTagName;
          xml.getDocumentElement().normalize();

          NodeList xmlNodes;
          if (tagName != null && !tagName.isEmpty()) {
            xmlNodes = xml.getElementsByTagName(tagName);
          } else {
            xmlNodes = xml.getDocumentElement().getChildNodes();
          }

          for (int i = 0; i < xmlNodes.getLength(); i++) {
            final Node item = xmlNodes.item(i);
            Platform.runLater(new Runnable() {

              @Override
              public void run() {
                add(item);

              }
            });
          }
          return this;
        }
      };



      return task;
    }
  }
}
