/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU
 * General Public License Version 3 only ("GPL").  
 * You may not use this file except in compliance with the License. 
 * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html
 * See the License for the specific language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 * 
 */

package org.jopendocument.dom.spreadsheet;

import org.jopendocument.dom.ContentType;
import org.jopendocument.dom.ContentTypeVersioned;
import org.jopendocument.dom.NS;
import org.jopendocument.dom.ODPackage;
import org.jopendocument.dom.OOUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Iterator;

import javax.swing.table.TableModel;

import org.apache.commons.collections.map.LinkedMap;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.xpath.XPath;

/**
 * A calc document.
 * 
 * @author Sylvain
 */
public class SpreadSheet {

    public static SpreadSheet createFromFile(File f) throws IOException {
        final ODPackage fd = new ODPackage(f);
        return new SpreadSheet(fd.getDocument("content.xml"), fd.getDocument("styles.xml"), fd);
    }

    public static SpreadSheet createEmpty(TableModel t) throws IOException {
        return createEmpty(t, NS.getOD());
    }

    public static SpreadSheet createEmpty(TableModel t, NS ns) throws IOException {
        final Document doc = new Document(new Element("document-content", ns.getOFFICE()));
        final ContentTypeVersioned ct = ContentTypeVersioned.fromType(ContentType.SPREADSHEET, ns.getVersion());
        if (ct.getVersion().equals(OOUtils.VERSION_1)) {
            doc.getRootElement().setAttribute("class", ct.getShortName(), ns.getOFFICE());
        }
        // don't forget that, otherwise OO crash
        doc.getRootElement().addContent(new Element("automatic-styles", ns.getOFFICE()));

        final Element topBody = new Element("body", ns.getOFFICE());
        final Element body;
        if (ct.getVersion().equals(OOUtils.VERSION_2)) {
            body = new Element(ct.getShortName(), ns.getOFFICE());
            topBody.addContent(body);
        } else
            body = topBody;
        doc.getRootElement().addContent(topBody);
        final Element sheetElem = Sheet.createEmpty(ns);
        body.addContent(sheetElem);

        final SpreadSheet spreadSheet = new SpreadSheet(doc, null);
        spreadSheet.getSheet(0).merge(t, 0, 0, true);
        return spreadSheet;
    }

    /**
     * Export the passed data to file.
     * 
     * @param t the data to export.
     * @param f where to export, if the extension is missing (or wrong) the correct one will be
     *        added, eg "dir/data".
     * @param ns the version of XML.
     * @return the saved file, eg "dir/data.ods".
     * @throws IOException if the file can't be saved.
     */
    public static File export(TableModel t, File f, NS ns) throws IOException {
        return SpreadSheet.createEmpty(t, ns).saveAs(f);
    }

    // may be null
    private final ODPackage originalFile;
    private final String version;
    // content.xml
    private final Document doc;
    // styles.xml
    private final Document styles;

    private final LinkedMap sheets;

    public SpreadSheet(Document doc, Document styles) {
        this(doc, styles, null);
    }

    private SpreadSheet(Document doc, Document styles, ODPackage orig) {
        this.doc = doc;
        this.styles = styles;
        // OOFileDocument
        if (orig != null) {
            // ATTN OK because this is our private instance (see createFromFile())
            this.originalFile = orig;
        } else {
            this.originalFile = new ODPackage();
        }
        this.originalFile.putFile("content.xml", this.doc);
        if (this.styles != null)
            this.originalFile.putFile("styles.xml", this.styles);

        this.version = NS.getVersion(doc);
        this.sheets = new LinkedMap();
        final Iterator iter = this.getBody().getChildren("table", this.getNS().getTABLE()).iterator();
        while (iter.hasNext()) {
            final Element t = (Element) iter.next();
            this.sheets.put(t.getAttributeValue("name", this.getNS().getTABLE()), t);
        }
    }

    private Element getBody() {
        final Element body = this.doc.getRootElement().getChild("body", this.getNS().getOFFICE());
        if (this.version.equals(OOUtils.VERSION_1))
            return body;
        else
            return body.getChild("spreadsheet", this.getNS().getOFFICE());
    }

    public final Element getAutoStyle(String name) {
        try {
            final XPath xp = this.getXPath("./office:automatic-styles/style:style[@style:name='" + name + "']");
            return (Element) xp.selectSingleNode(this.doc.getRootElement());
        } catch (JDOMException e) {
            throw new IllegalStateException("", e);
        }
    }

    protected final String[] resolve(String namedRange) {
        final Attribute attr;
        try {
            final XPath path = this.getXPath("./table:named-expressions/table:named-range[@table:name='" + namedRange + "']/@table:base-cell-address");
            attr = (Attribute) path.selectSingleNode(this.getBody());
        } catch (JDOMException e) {
            throw new IllegalStateException();
        }
        if (attr != null) {
            // $facture.$H$34
            final String ref = attr.getValue().replaceAll("\\$", "");
            final int dotIndex = ref.indexOf('.');
            final String sheetName = ref.substring(0, dotIndex);
            final String cellName = ref.substring(dotIndex + 1);
            return new String[] { sheetName, cellName };
        } else
            return null;
    }

    public Object getValueAt(String ref) {
        // OO doesn't allow cellRef as range names
        if (Sheet.isCellRef(ref)) {
            throw new IllegalArgumentException(ref + " is a cell range, you must use it on a sheet");
        } else {
            final String[] r = this.resolve(ref);
            return this.getSheet(r[0]).getValueAt(r[1]);
        }
    }

    protected final NS getNS() {
        return NS.get(this.version);
    }

    public XPath getXPath(String p) throws JDOMException {
        return OOUtils.getXPath(p, this.version);
    }

    public int getSheetCount() {
        return this.sheets.size();
    }

    public Sheet getSheet(int i) {
        return this.getSheet((String) this.sheets.get(i));
    }

    public Sheet getSheet(String name) {
        Object res = this.sheets.get(name);
        if (res instanceof Element) {
            res = new Sheet(this, (Element) res);
            this.sheets.put(name, res);
        }
        return (Sheet) res;
    }

    // *** Files

    /**
     * Put a data file into this spreadsheet.
     * 
     * @param name the entry name in the zip.
     * @param data the content.
     */
    void putFile(String name, byte[] data) {
        synchronized (this.originalFile) {
            this.originalFile.putFile(name, data);
        }
    }

    public File saveAs(File file) throws FileNotFoundException, IOException {
        synchronized (this.originalFile) {
            this.originalFile.setFile(file);
            return this.originalFile.save();
        }
    }

}