/* ====================================================================
 *
 * The ObjectStyle Group Software License, Version 1.0
 *
 * Copyright (c) 2005 The ObjectStyle Group,
 * and individual authors of the software.  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 end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        ObjectStyle Group (http://objectstyle.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "ObjectStyle Group" and "Cayenne"
 *    must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact andrus@objectstyle.org.
 *
 * 5. Products derived from this software may not be called "ObjectStyle"
 *    nor may "ObjectStyle" appear in their names without prior written
 *    permission of the ObjectStyle Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 OBJECTSTYLE GROUP OR
 * ITS CONTRIBUTORS 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the ObjectStyle Group.  For more
 * information on the ObjectStyle Group, please see
 * <http://objectstyle.org/>.
 *
 */
package org.objectstyle.wolips.bindings.api;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.URL;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.objectstyle.wolips.locate.LocatePlugin;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

public class ApiModel {
  private Document _document;

  private URL _url;

  private Reader _reader;

  private IFile _eclipseFile;

  private File _file;

  private boolean _isDirty;

  private long _lastModified;

  public ApiModel(File file) throws ApiModelException {
    _file = file;
    if (!file.exists() || file.length() == 0) {
      String javaFileName = LocatePlugin.getDefault().fileNameWithoutExtension(file);
      try {
        FileWriter writer = new FileWriter(file);
        try {
          writer.write(ApiModel.blankContent(javaFileName));
        }
        finally {
          writer.close();
        }
      }
      catch (IOException e) {
        throw new ApiModelException("Failed to create blank API file.", e);
      }
    }
    parse();
  }

  public ApiModel(IFile file) throws ApiModelException {
    _eclipseFile = file;
    _file = file.getLocation().toFile();
    if (!file.exists() || file.getLocation().toFile().length() == 0) {
      String javaFileName = LocatePlugin.getDefault().fileNameWithoutExtension(file);
      try {
        _document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(blankContent(javaFileName))));
        saveChanges();
      }
      catch (Exception e) {
        throw new ApiModelException("Failed to create blank API file.", e);
      }
    }
    _lastModified = _eclipseFile.getModificationStamp();
    parse();
  }

  public ApiModel(URL url) throws ApiModelException {
    _url = url;
    parse();
  }

  public ApiModel(Reader reader) throws ApiModelException {
    _reader = reader;
    parse();
  }

  public static String blankContent(String name) {
    StringBuffer sb = new StringBuffer();
    sb.append("<?xml version = \"1.0\" encoding = \"UTF-8\" standalone = \"yes\"?>\n");
    sb.append("<wodefinitions>\n");
    sb.append("    <wo wocomponentcontent = \"false\" class = \"" + name + "\">");
    sb.append("    </wo>\n");
    sb.append("</wodefinitions>\n");
    return sb.toString();
  }

  public IFile getEclipseFile() {
    return _eclipseFile;
  }

  public String getLocation() {
    String location;
    if (_url != null) {
      location = _url.toExternalForm();
    }
    else if (_file != null) {
      location = _file.getAbsolutePath();
    }
    else {
      location = null;
    }
    return location;
  }

  public boolean parseIfNecessary() throws ApiModelException {
    synchronized (this) {
      boolean parsed = false;
      if (_eclipseFile != null) {
        if (_eclipseFile.getModificationStamp() != _lastModified) {
          parse();
          parsed = true;
        }
      }
      else if (_file != null) {
        if (_file.lastModified() != _lastModified) {
          parse();
          parsed = true;
        }
      }
      return parsed;
    }
  }

  private void parse() throws ApiModelException {
    synchronized (this) {
      try {
        if (_eclipseFile != null) {
          _lastModified = _eclipseFile.getModificationStamp();
        }
        else if (_file != null) {
          _lastModified = _file.lastModified();
        }

        DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        if (_url != null) {
          _document = documentBuilder.parse(_url.toExternalForm());
        }
        else if (_file != null) {
          // MS: This is really silly, but I guess WOBuilder (or something
          // else back in the day) produced
          // API files that claim to be encoding 'macintosh'. Well, there
          // is no such encoding, at
          // least as far as Java is concerned. So we proprocess the api
          // file and switch out the
          // encoding for UTF-8 instead. Super lame. I know.
          StringBuffer apiBuffer = new StringBuffer();
          FileReader apiReader = new FileReader(_file);
          try {
            BufferedReader bufferedApiReader = new BufferedReader(apiReader);
            String line = bufferedApiReader.readLine();
            if (line != null && line.startsWith("<?")) {
              apiBuffer.append(line.replaceAll("encoding=\"macintosh\"", "encoding=\"UTF-8\""));
              apiBuffer.append("\n");
            }
            while ((line = bufferedApiReader.readLine()) != null) {
              apiBuffer.append(line);
              apiBuffer.append("\n");
            }
          }
          finally {
            apiReader.close();
          }
          StringReader apiBufferReader = new StringReader(apiBuffer.toString());
          _document = documentBuilder.parse(new InputSource(apiBufferReader));
        }
        else if (_reader != null) {
          _document = documentBuilder.parse(new InputSource(_reader));
        }
        else {
          throw new ApiModelException("There was no file, URL, or reader specified as the location for this API file.");
        }
        _document.normalize();
        removeTextNodes(_document);
      }
      catch (Throwable e) {
        throw new ApiModelException("Failed to parse API file '" + getLocation() + "'.", e);
      }
    }
  }

  private void removeTextNodes(Node node) {
    NodeList children = node.getChildNodes();
    for (int i = children.getLength() - 1; i >= 0; i--) {
      Node child = children.item(i);
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        removeTextNodes(child);
      } else if (child.getNodeType() == Node.TEXT_NODE) {
        node.removeChild(child);
      }
	}
  }

  public Wodefinitions getWODefinitions() throws ApiModelException {
    synchronized (this) {
      parseIfNecessary();
      Element element = _document.getDocumentElement();
      return new Wodefinitions(element, this);
    }
  }

  public Wo getWo() throws ApiModelException {
    Wodefinitions wodefinitions = getWODefinitions();
    if (wodefinitions == null) {
      return null;
    }
    return wodefinitions.getWo();
  }

  public void saveChanges() throws ApiModelException {
    if (_file == null) {
      throw new ApiModelException("You can not saveChanges to an ApiModel that is not backed by a file.");
    }
    try {
      FileWriter writer = new FileWriter(_file);
      try {
        saveChanges(writer);
      }
      finally {
        writer.close();
      }
      if (_eclipseFile != null) {
        try {
          _eclipseFile.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
        }
        catch (CoreException e) {
          // ignore
        }
      }
    }
    catch (IOException ioe) {
      throw new ApiModelException("Failed to save changes to API file.", ioe);
    }
  }

  public void saveChanges(Writer writer) throws ApiModelException {
    try {
      TransformerFactory transformerFactory = TransformerFactory.newInstance();
      transformerFactory.setAttribute("indent-number", Integer.valueOf(4));
      Transformer transformer = transformerFactory.newTransformer();
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
      transformer.setOutputProperty(OutputKeys.METHOD, "xml");
      transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
      DOMSource domsource = new DOMSource(_document);
      StreamResult output = new StreamResult(writer);
      transformer.transform(domsource, output);
      _isDirty = false;
    }
    catch (Throwable t) {
      throw new ApiModelException("Failed to save API file " + getLocation() + ".", t);
    }
  }

  public boolean isDirty() {
    return _isDirty;
  }

  public void markAsDirty() {
    _isDirty = true;
  }
}