/*
 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.javafx.css;

import com.sun.javafx.collections.TrackableObservableList;
import com.sun.javafx.css.parser.CSSParser;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.StyleOrigin;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * A stylesheet which can apply properties to a tree of objects.  A stylesheet
 * is a collection of zero or more {@link Rule Rules}, each of which is applied
 * to each object in the tree.  Typically the selector will examine the object to
 * determine whether or not it is applicable, and if so it will apply certain
 * property values to the object.
 * <p>
 */
public class Stylesheet {

    /**
     * Version number of binary CSS format. The value is incremented whenever the format of the
     * binary stream changes. This number does not correlate with JavaFX versions.
     * Version 5: persist @font-face
     */
    final static int BINARY_CSS_VERSION = 5;

    private final String url;
    /** The URL from which the stylesheet was loaded.
     * @return The URL from which the stylesheet was loaded, or null if
     *         the stylesheet was created from an inline style.
     */
    public String getUrl() {
        return url;
    }

    /**
     * True if this style came from user stylesheet, we need to know this so
     * that we can make user important styles have higher priority than
     * author styles
     */
    private StyleOrigin origin = StyleOrigin.AUTHOR;
    public StyleOrigin getOrigin() {
        return origin;
    }
    public void setOrigin(StyleOrigin origin) {
        this.origin = origin;
    }

    /** All the rules contained in the stylesheet in the order they are in the file */
    private final ObservableList<Rule> rules = new TrackableObservableList<Rule>() {

        @Override
        protected void onChanged(Change<Rule> c) {
            c.reset();
            while (c.next()) {
                if (c.wasAdded()) {
                    for(Rule rule : c.getAddedSubList()) {
                        rule.setStylesheet(Stylesheet.this);
                    }
                } else if (c.wasRemoved()) {
                    for (Rule rule : c.getRemoved()) {
                        if (rule.getStylesheet() == Stylesheet.this) rule.setStylesheet(null);
                    }
                }
            }
        }
    };

    /** List of all font faces */
    private final List<FontFace> fontFaces = new ArrayList<FontFace>();

    /**
     * Constructs a stylesheet with the base URI defaulting to the root
     * path of the application.
     */
    public Stylesheet() {

//        ClassLoader cl = Thread.currentThread().getContextClassLoader();
//        this.url = (cl != null) ? cl.getResource("") : null;
        //
        // RT-17344
        // The above code is unreliable. The getResource call is intended
        // to return the root path of the Application instance, but it sometimes
        // returns null. Here, we'll set url to null and then when a url is
        // resolved, the url path can be used in the getResource call. For
        // example, if the css is -fx-image: url("images/duke.png"), we can
        // do cl.getResouce("images/duke.png") in URLConverter
        //

        this(null);
    }

    /**
     * Constructs a Stylesheet using the given URL as the base URI. The
     * parameter may not be null.
     * @param url
     */
    public Stylesheet(String url) {

        this.url = url;

    }

    public List<Rule> getRules() {
        return rules;
    }

    public List<FontFace> getFontFaces() {
        return fontFaces;
    }

    @Override public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj instanceof Stylesheet) {
            Stylesheet other = (Stylesheet)obj;

            if (this.url == null && other.url == null) {
                return true;
            } else if (this.url == null || other.url == null) {
                return false;
            } else {
                return this.url.equals(other.url);
            }
        }
        return false;
    }

    @Override public int hashCode() {
        int hash = 7;
        hash = 13 * hash + (this.url != null ? this.url.hashCode() : 0);
        return hash;
    }

    /** Returns a string representation of this object. */
    @Override public String toString() {
        StringBuilder sbuf = new StringBuilder();
        sbuf.append("/* ");
        if (url != null) sbuf.append(url);
        if (rules.isEmpty()) {
            sbuf.append(" */");
        } else {
            sbuf.append(" */\n");
            for(int r=0; r<rules.size(); r++) {
                sbuf.append(rules.get(r));
                sbuf.append('\n');
            }
        }
        return sbuf.toString();
    }

    // protected for unit testing
    final void writeBinary(final DataOutputStream os, final StringStore stringStore)
        throws IOException
    {
        // Note: url is not written since it depends on runtime environment.
        int index = stringStore.addString(origin.name());
        os.writeShort(index);
        os.writeShort(rules.size());
        for (Rule r : rules) r.writeBinary(os,stringStore);

        // Version 5 adds persistence of FontFace
        List<FontFace> fontFaceList = getFontFaces();
        int nFontFaces = fontFaceList != null ? fontFaceList.size() : 0;
        os.writeShort(nFontFaces);

        for(int n=0; n<nFontFaces; n++) {
            FontFace fontFace = fontFaceList.get(n);
            fontFace.writeBinary(os, stringStore);
        }
    }

    // protected for unit testing
    final void readBinary(int bssVersion, DataInputStream is, String[] strings)
        throws IOException
    {
        this.stringStore = strings;
        final int index = is.readShort();
        this.setOrigin(StyleOrigin.valueOf(strings[index]));
        final int nRules = is.readShort();
        List<Rule> persistedRules = new ArrayList<Rule>(nRules);
        for (int n=0; n<nRules; n++) {
            persistedRules.add(Rule.readBinary(bssVersion,is,strings));
        }
        this.rules.addAll(persistedRules);

        if (bssVersion >= 5) {
            List<FontFace> fontFaceList = this.getFontFaces();
            int nFontFaces = is.readShort();
            for (int n=0; n<nFontFaces; n++) {
                FontFace fontFace = FontFace.readBinary(bssVersion, is, strings);
                fontFaceList.add(fontFace);
            }
        }
    }

    private String[] stringStore;
    final String[] getStringStore() { return stringStore; }

    /** Load a binary stylesheet file from a input stream */
    public static Stylesheet loadBinary(URL url) throws IOException {

        if (url == null) return null;

        Stylesheet stylesheet = null;

        try (DataInputStream dataInputStream =
                     new DataInputStream(new BufferedInputStream(url.openStream(), 40 * 1024))) {

            // read file version
            final int bssVersion = dataInputStream.readShort();
            if (bssVersion > Stylesheet.BINARY_CSS_VERSION) {
                throw new IOException(url.toString() + " wrong binary CSS version: "
                        + bssVersion + ". Expected version less than or equal to" +
                        Stylesheet.BINARY_CSS_VERSION);
            }
            // read strings
            final String[] strings = StringStore.readBinary(dataInputStream);
            // read binary data
            stylesheet = new Stylesheet(url.toExternalForm());

            try {

                dataInputStream.mark(Integer.MAX_VALUE);
                stylesheet.readBinary(bssVersion, dataInputStream, strings);

            } catch (Exception e) {

                stylesheet = new Stylesheet(url.toExternalForm());

                dataInputStream.reset();

                if (bssVersion == 2) {
                    // RT-31022
                    stylesheet.readBinary(3, dataInputStream, strings);
                } else {
                    stylesheet.readBinary(Stylesheet.BINARY_CSS_VERSION, dataInputStream, strings);
                }
            }

        } catch (FileNotFoundException fnfe) {
            // This comes from url.openStream() and is expected.
            // It just means that the .bss file doesn't exist.
        }

        // return stylesheet
        return stylesheet;
    }

    /**
     * Convert the .css file referenced by urlIn to binary format and write to urlOut.
     * @param source is the JavaFX .css file to convert
     * @param destination is the file to which the binary conversion is written
     * @throws IOException
     * @throws IllegalArgumentException if either parameter is null, if source and destination are the same,
     * if source cannot be read, or if destination cannot be written.
     */
    public static void convertToBinary(File source, File destination) throws IOException {

        if (source == null || destination == null) {
            throw new IllegalArgumentException("parameters may not be null");
        }

        if (source.getAbsolutePath().equals(destination.getAbsolutePath())) {
            throw new IllegalArgumentException("source and destination may not be the same");
        }

        if (source.canRead() == false) {
            throw new IllegalArgumentException("cannot read source file");
        }

        if (destination.exists() ? (destination.canWrite() == false) : (destination.createNewFile() == false)) {
            throw new IllegalArgumentException("cannot write destination file");
        }

        URI sourceURI = source.toURI();
        Stylesheet stylesheet = new CSSParser().parse(sourceURI.toURL());

        // first write all the css binary data into the buffer and collect strings on way
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        StringStore stringStore = new StringStore();
        stylesheet.writeBinary(dos, stringStore);
        dos.flush();
        dos.close();

        FileOutputStream fos = new FileOutputStream(destination);
        DataOutputStream os = new DataOutputStream(fos);

        // write file version
        os.writeShort(BINARY_CSS_VERSION);

        // write strings
        stringStore.writeBinary(os);

        // write binary css
        os.write(baos.toByteArray());
        os.flush();
        os.close();
    }

    // Add the rules from the other stylesheet to this one
    public void importStylesheet(Stylesheet importedStylesheet) {
        if (importedStylesheet == null) return;

        List<Rule> rulesToImport = importedStylesheet.getRules();
        if (rulesToImport == null || rulesToImport.isEmpty()) return;

        List<Rule> importedRules = new ArrayList<>(rulesToImport.size());
        for (Rule rule : rulesToImport) {
            List<Selector> selectors = rule.getSelectors();
            List<Declaration> declarations = rule.getUnobservedDeclarationList();
            importedRules.add(new Rule(selectors, declarations));
        }

        rules.addAll(importedRules);
    }
}
