/*
 * 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;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import org.jopendocument.dom.spreadsheet.Table;
import org.jopendocument.dom.text.Heading;
import org.jopendocument.dom.text.Paragraph;
import org.jopendocument.dom.text.ParagraphStyle;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;

import junit.framework.TestCase;

import org.jdom.Element;
import org.jdom.xpath.XPath;

public class ODSingleXMLDocumentTest extends TestCase {

    protected void setUp() throws Exception {
        super.setUp();
    }

    public void testCreate() throws Exception {
        final ODSingleXMLDocument single = new ODPackage(this.getClass().getResourceAsStream("test.odt")).toSingle();
        assertNull(single.isValid());
        assertNull(single.checkStyles());
    }

    public void testAdd() throws Exception {
        final ODSingleXMLDocument single = new ODPackage(this.getClass().getResourceAsStream("empty.odt")).toSingle();
        // really empty
        single.getBody().removeContent();
        final ODSingleXMLDocument single2 = new ODPackage(this.getClass().getResourceAsStream("styles.odt")).toSingle();
        single.add(single2);
        assertNull(single.isValid());
        assertNull(single.checkStyles());
    }

    public void testAddParagraph() throws Exception {
        final ODSingleXMLDocument single = new ODPackage(this.getClass().getResourceAsStream("styles.odt")).toSingle();

        final Paragraph p = new Paragraph();
        p.setStyle("testPragraph");
        p.addContent("Hello");
        p.addTab();
        p.addStyledContent("World", "testChar");
        // add at the end
        assertNull(p.getElement().getDocument());
        p.setDocument(single);
        assertSame(single.getDocument(), p.getElement().getDocument());

        final Heading h = new Heading();
        h.setStyle("inexistantt");
        h.addContent("Heading text");
        try {
            h.setDocument(single);
            fail("should throw since style doesn't exist");
        } catch (Exception e) {
            // ok
        }
        h.setStyle("testPragraph");
        // add before p
        final Element pParent = p.getElement().getParentElement();
        single.add(h, pParent, pParent.indexOf(p.getElement()));

        assertNull(single.isValid());
        assertNull(single.checkStyles());

        // rm
        p.setDocument(null);
        assertNull(p.getElement().getDocument());
    }

    public void testTable() throws Exception {
        final ODSingleXMLDocument single = new ODPackage(this.getClass().getResourceAsStream("test.odt")).toSingle();
        assertNull(single.getDescendantByName("draw:text-box", "inexistant"));
        final Element table = single.getDescendantByName("table:table", "JODTestTable");
        assertNotNull(table);
        final Table<ODSingleXMLDocument> t = new Table<ODSingleXMLDocument>(single, table);

        assertEquals(1, t.getHeaderRowCount());
        assertEquals(0, t.getHeaderColumnCount());

        final Calendar c = Calendar.getInstance();
        c.clear();
        c.set(2005, 0, 12, 12, 35);
        assertEquals(c.getTime(), t.getValueAt(2, 1));

        // 11.91cm
        assertEquals(119.06f, t.getWidth());
        t.setColumnCount(6, -1, true);
        t.setValueAt(3.14, 5, 0);
        assertTableWidth(t, 119.06f);
        final float ratio = t.getColumn(0).getWidth() / t.getColumn(1).getWidth();
        t.setColumnCount(2, 1, true);
        // ratio is kept
        assertEquals(ratio, t.getColumn(0).getWidth() / t.getColumn(1).getWidth());
        assertTableWidth(t, 119.06f);
        // table changes width
        final float width = t.getColumn(0).getWidth();
        t.setColumnCount(4, 0, false);
        assertTableWidth(t, 119.06f + 2 * width);
        t.setColumnCount(1, 123, false);
        assertEquals(1, t.getColumnCount());
        assertTableWidth(t, width);

        assertNull(single.isValid());
        assertNull(single.checkStyles());

        t.detach();
        assertNull(single.getDescendantByName("table:table", "JODTestTable"));
    }

    private void assertTableWidth(Table<?> t, float w) {
        assertEquals(w, t.getWidth());
        float total = 0;
        for (int i = 0; i < t.getColumnCount(); i++) {
            total += t.getColumn(i).getWidth();
        }
        assertEquals(round(w), round(total));
    }

    private long round(float w) {
        return Math.round(w * 100.0) / 100;
    }

    public void testFrame() throws Exception {
        final ODPackage pkg = new ODPackage(this.getClass().getResourceAsStream("test.odt"));
        final Element frameElem = pkg.toSingle().getDescendantByName("draw:frame", "Cadre1");

        final ODFrame<ODSingleXMLDocument> frame = new ODFrame<ODSingleXMLDocument>(pkg.toSingle(), frameElem);
        assertEquals(new BigDecimal("42.72"), frame.getWidth());
        // height depends on the content
        assertNull(frame.getHeight());

        assertEquals("right", frame.getStyle().getGraphicProperties().getHorizontalPosition());
        assertEquals("paragraph", frame.getStyle().getGraphicProperties().getHorizontalRelation());

        assertEquals(asList("position"), frame.getStyle().getGraphicProperties().getProtected());
        assertTrue(frame.getStyle().getGraphicProperties().isContentPrinted());
    }

    public void testStyle() throws Exception {
        {
            final ODPackage pkg = new ODPackage(this.getClass().getResourceAsStream("test.odt"));

            // test that multiple attributes may refer to paragraph styles
            final XPath ellipseXPath = pkg.getContent().getXPath("//draw:ellipse[@draw:name = 'Ellipse']");
            final Element ellipse = (Element) ellipseXPath.selectSingleNode(pkg.getContent().getDocument());
            final String drawTextStyleName = ellipse.getAttributeValue("text-style-name", ellipse.getNamespace("draw"));
            final ParagraphStyle ellipseTextStyle = ParagraphStyle.DESC.findStyle(pkg, pkg.getContent().getDocument(), drawTextStyleName);
            assertEquals(singletonList(ellipse), ellipseTextStyle.getReferences());

            // P1 exists both in content and styles but doesn't denote the same style
            final ODXMLDocument styles = pkg.getXMLFile("styles.xml");
            final XPath headerXPath = styles.getXPath("//text:p[string() = 'Header']");
            final Element headerElem = (Element) headerXPath.selectSingleNode(styles.getDocument());
            final String styleName = new Paragraph(headerElem).getStyleName();

            final Element styleP1 = pkg.getStyle(pkg.getDocument("styles.xml"), "paragraph", styleName);
            final Element contentP1 = pkg.getStyle(pkg.getDocument("content.xml"), "paragraph", styleName);
            assertNotNull(styleP1);
            assertNotNull(contentP1);
            assertNotSame(styleP1, contentP1);

            // styleP1 can only be referenced from styles.xml
            assertEquals(singletonList(headerElem), StyleStyle.warp(pkg, styleP1).getReferences());
            // contentP1 can only be referenced from content.xml
            assertFalse(StyleStyle.warp(pkg, contentP1).getReferences().contains(headerElem));

            // when merging styles.xml
            pkg.toSingle();
            // the content doesn't change
            assertSame(contentP1, pkg.getStyle(pkg.getDocument("content.xml"), "paragraph", styleName));
            final String mergedStyleName = new Paragraph((Element) headerXPath.selectSingleNode(pkg.getContent().getDocument())).getStyleName();
            assertNotNull(mergedStyleName);
            assertFalse(styleName.equals(mergedStyleName));
        }

        final ODPackage pkg = new ODPackage(this.getClass().getResourceAsStream("test.odt"));
        final Element heading = (Element) pkg.getContent().getXPath("//text:h[string() = 'Titre 2']").selectSingleNode(pkg.getContent().getDocument());
        final String styleName = heading.getAttributeValue("style-name", heading.getNamespace());
        // common styles are not in content.xml
        assertNull(pkg.getContent().getStyle("paragraph", styleName));
        // but in styles.xml
        testStyleElem(pkg.getXMLFile("styles.xml").getStyle("paragraph", styleName));
        testStyleElem(pkg.getStyle("paragraph", styleName));
        // except if we merge the two
        pkg.toSingle();
        testStyleElem(pkg.getContent().getStyle("paragraph", styleName));
        testStyleElem(pkg.getStyle("paragraph", styleName));
    }

    private void testStyleElem(final Element styleElem) {
        assertNotNull(styleElem);
        assertEquals("2", styleElem.getAttributeValue("default-outline-level", styleElem.getNamespace()));
        assertEquals("Heading", styleElem.getAttributeValue("parent-style-name", styleElem.getNamespace()));
    }

    public void testMeta() throws Exception {
        final ODPackage pkg = new ODPackage(this.getClass().getResourceAsStream("test.odt"));
        final ODMeta meta = pkg.getMeta();
        assertEquals("firstInfo", meta.getUserMeta("Info 1").getValue());
        assertEquals("", meta.getUserMeta("secondName").getValue());

        final List<String> expected = Arrays.asList("Info 1", "secondName", "Info 3", "Info 4");
        assertEquals(expected, meta.getUserMetaNames());

        // does not exist
        assertNull(meta.getUserMeta("toto"));
        // now it does
        assertNotNull(meta.getUserMeta("toto", true));
        meta.removeUserMeta("toto");
        // now it was removed
        assertNull(meta.getUserMeta("toto"));
        final ODUserDefinedMeta toto = meta.getUserMeta("toto", true);
        toto.setValue(3.5);
        assertEquals(ODValueType.FLOAT, toto.getValueType());
        assertEquals(3.5f, ((BigDecimal) toto.getValue()).floatValue());
        final TimeZone marquisesTZ = TimeZone.getTimeZone("Pacific/Marquesas");
        final TimeZone pstTZ = TimeZone.getTimeZone("PST");
        final Calendar cal = Calendar.getInstance(pstTZ);
        final int hour = cal.get(Calendar.HOUR_OF_DAY);
        final int minute = cal.get(Calendar.MINUTE);

        {
            toto.setValue(cal, ODValueType.TIME);
            Calendar actual = (Calendar) toto.getValue();
            assertEquals(hour, actual.get(Calendar.HOUR_OF_DAY));
            assertEquals(minute, actual.get(Calendar.MINUTE));
            // for TIME the time zone is important, the same date has not the same value
            cal.setTimeZone(marquisesTZ);
            toto.setValue(cal, ODValueType.TIME);
            actual = (Calendar) toto.getValue();
            // +1h30
            assertFalse(hour == actual.get(Calendar.HOUR_OF_DAY));
            assertFalse(minute == actual.get(Calendar.MINUTE));
        }

        {
            cal.setTimeZone(pstTZ);
            toto.setValue(cal, ODValueType.DATE);
            assertEquals(cal.getTime(), toto.getValue());
            // on the contrary for DATE the timezone is irrelevant
            cal.setTimeZone(marquisesTZ);
            toto.setValue(cal, ODValueType.DATE);
            assertEquals(cal.getTime(), toto.getValue());
        }
    }
}
