/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.nbbuild;

import java.io.*;
import java.util.*;
import java.io.FileOutputStream;
import java.util.zip.CRC32;

import org.w3c.dom.*;
import org.xml.sax.InputSource;

import org.apache.tools.ant.BuildException;

/** This class represents module updates tracking
 *
 * @author  akemr
 */
class UpdateTracking {
    private static final String ELEMENT_MODULE = "module"; // NOI18N
    private static final String ATTR_CODENAME = "codename"; // NOI18N
    private static final String ELEMENT_VERSION = "module_version"; // NOI18N
    private static final String ATTR_VERSION = "specification_version"; // NOI18N
    private static final String ATTR_ORIGIN = "origin"; // NOI18N
    private static final String ATTR_LAST = "last"; // NOI18N
    private static final String ATTR_INSTALL = "install_time"; // NOI18N
    private static final String ELEMENT_FILE = "file"; // NOI18N
    private static final String ATTR_FILE_NAME = "name"; // NOI18N
    private static final String ATTR_CRC = "crc"; // NOI18N
    
    private static final String NBM_ORIGIN = "nbm"; // NOI18N
    private static final String INST_ORIGIN = "installer"; // NOI18N

    /** Platform dependent file name separator */
    private static final String FILE_SEPARATOR = System.getProperty ("file.separator");  // NOI18N           

    /** The name of the install_later file */
    public static final String TRACKING_DIRECTORY = "update_tracking"; // NOI18N
    
    private File trackingFile = null;
    
    private String origin = NBM_ORIGIN;
    private String nbPath = null;
    private Module module = null;
    protected InputStream is = null;
    protected OutputStream os = null;
   
    // for generating xml in build process
    public UpdateTracking( String nbPath ) {
        this.nbPath = nbPath;
        origin = INST_ORIGIN;
    }

    /**
     * Use this constructor, only when you want to use I/O Streams
     */
    public UpdateTracking () {
        this.nbPath = null;
        origin = INST_ORIGIN;
    }
    
    public Version addNewModuleVersion( String codename, String spec_version ) {
        module = new Module();
        module.setCodename( codename );
        Version version = new Version();        
        version.setVersion( spec_version );
        version.setOrigin( origin );
        version.setLast( true );
        version.setInstall_time( System.currentTimeMillis() );
        module.setVersion( version );
        return version;
    }
    
    public String getVersionFromFile (File utf) throws BuildException {
        this.setTrackingFile(utf.getParentFile(), utf.getName());
        read();
        if ( module.getVersions().size() != 1 ) 
            throw new BuildException ("Module described in update tracking file " + utf.getAbsolutePath() + " has got " + module.getVersions().size() + " specification versions. Correct number is 1.");
        return module.getVersions().get(0).getVersion();
    }
    
    public String getCodenameFromFile (File utf) throws BuildException {
        this.setTrackingFile(utf.getParentFile(), utf.getName());
        read();
        if ( module.getVersions().size() != 1 ) 
            throw new BuildException ("Module described in update tracking file " + utf.getAbsolutePath() + " has got " + module.getVersions().size() + " specification versions. Correct number is 1.");
        return module.getCodename();
    }
    
    public String getVersionForCodeName( String codeName ) throws BuildException {
        module = new Module();
        module.setCodename( codeName );
//        if (this.is == null) {
            File directory = new File( nbPath + FILE_SEPARATOR + TRACKING_DIRECTORY );
            setTrackingFile(directory, getTrackingFileName());
            if (!trackingFile.exists() || !trackingFile.isFile())
                throw new BuildException ("Tracking file " + trackingFile.getAbsolutePath() + " cannot be found for module " + module.getCodenamebase());
//        }
        read();
        if ( module.getVersions().size() != 1 ) 
            throw new BuildException ("Module with codenamebase " + codeName + " has got " + module.getVersions().size() + " specification versions. Correct number is 1.");
        return module.getVersions().get(0).getVersion();
    }
    
    public String[] getListOfNBM( String codeName ) throws BuildException {
        module = new Module();
        module.setCodename( codeName );
        if (this.is == null) {
            File directory = new File( nbPath + FILE_SEPARATOR + TRACKING_DIRECTORY );
            setTrackingFile(directory, getTrackingFileName());
            if (!trackingFile.exists() || !trackingFile.isFile())
                throw new BuildException ("Tracking file " + trackingFile.getAbsolutePath() + " cannot be found for module " + module.getCodenamebase());
        }
        
        read();
        
        if ( module.getVersions().size() != 1 ) 
            throw new BuildException ("Module with codenamebase " + codeName + " has got " + module.getVersions().size() + " specification versions. Correct number is 1.");
        
        List<ModuleFile> files = module.getVersions().get(0).getFiles();
        String [] listFiles = new String[ files.size() ];
        for (int i=0; i < files.size(); i++) {
            listFiles[i] = files.get(i).getName().replace(File.separatorChar,'/');
        }
        
        return listFiles;
    }

    public void removeLocalized( String locale ) {
        File updateDirectory = new File( nbPath, TRACKING_DIRECTORY );
        File[] trackingFiles = updateDirectory.listFiles( new FileFilter() { // Get only *.xml files
            public boolean accept( File file ) {
                return file.isFile() &&file.getName().endsWith(".xml"); //NOI18N
            }
        } );
        if (trackingFiles != null)
            for (int i = trackingFiles.length-1; i >= 0; i--) {
                trackingFile = trackingFiles[i];
                read();
                module.removeLocalized( locale );
                write();
            }
    }
    
    void write( ) throws BuildException{
        Document document = XMLUtil.createDocument(ELEMENT_MODULE);  
        Element e_module = document.getDocumentElement();
        e_module.setAttribute(ATTR_CODENAME, module.getCodename());
        for (Version ver : module.getVersions()) {
            Element e_version = document.createElement(ELEMENT_VERSION);
            e_version.setAttribute(ATTR_VERSION, ver.getVersion());
            e_version.setAttribute(ATTR_ORIGIN, ver.getOrigin());
            e_version.setAttribute(ATTR_LAST, "true");                          //NOI18N
            e_version.setAttribute(ATTR_INSTALL, Long.toString(ver.getInstall_time()));
            e_module.appendChild( e_version );
            for (ModuleFile file : ver.getFiles()) {
                Element e_file = document.createElement(ELEMENT_FILE);
                e_file.setAttribute(ATTR_FILE_NAME, file.getName().replace(File.separatorChar,'/'));
                e_file.setAttribute(ATTR_CRC, file.getCrc());
                e_version.appendChild( e_file );
            }
        }
        
        //document.getDocumentElement().normalize();
        if (this.os == null) {
            File directory = new File( nbPath + FILE_SEPARATOR + TRACKING_DIRECTORY );
            if (!directory.exists()) {
                directory.mkdirs();
            }
            setTrackingFile(directory, this.getTrackingFileName());
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(new File(directory,this.getTrackingFileName()));
            } catch (Exception e) {
                throw new BuildException("Could not get outputstream to write update tracking", e);
            }
            this.setTrackingOutputStream(fos);
        }
        try {
            try {
                XMLUtil.write(document, this.os);
            } finally {
                this.os.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
            if ((trackingFile != null) && (trackingFile.exists()))
                trackingFile.delete();
            throw new BuildException("Could not write update tracking", e);
        }        
    }

    protected void setTrackingFile (File dir, String tFname) throws BuildException {
        this.trackingFile = new File(dir,tFname);
//        this.trackingFile.mkdirs();
        try {
            //setTrackingOutputStream(new FileOutputStream(this.trackingFile));
            if (this.trackingFile.exists())
                setTrackingInputStream(new FileInputStream(this.trackingFile));
        } catch (java.io.FileNotFoundException fnf) {
            throw new BuildException("Unable to find tracking file "+this.trackingFile.getAbsolutePath(), fnf);
        }
    }
    
    public void setTrackingOutputStream(OutputStream tos) {
        this.os = tos;
    }
    
    public OutputStream getTrackingOutputStream() {
        return this.os;
    }
    
    public void setTrackingInputStream(InputStream tis) {
        this.is = tis;
    }
    
    public String getTrackingFileName() throws BuildException {
        String trackingFileName = module.getCodenamebase();
        if ( ( trackingFileName == null ) || ( trackingFileName.length() == 0 ) )
            throw new BuildException ("Empty codenamebase, unable to locate tracking file");
        trackingFileName = trackingFileName.replace('.', '-') + ".xml"; //NOI18N
        return trackingFileName;
    }

    /** Scan through Document document. */
    private void read() throws BuildException {
        /** Document document */
        Document document;
        if (this.is == null) {
            File directory = new File( nbPath + FILE_SEPARATOR + TRACKING_DIRECTORY );
            if (!directory.exists()) {
                directory.mkdirs();
            }
            setTrackingFile(directory,getTrackingFileName());
        }
        try {
            InputSource xmlInputSource = new InputSource( this.is );
            document = XMLUtil.parse( xmlInputSource, false, false, XMLUtil.rethrowHandler(), XMLUtil.nullResolver());
            if (is != null)
                is.close();
        } catch ( org.xml.sax.SAXException e ) {
            e.printStackTrace();
            if (trackingFile == null) {
                throw new BuildException ("Update tracking data in external InputStream is not well formatted XML document.", e);
            } else {
                throw new BuildException ("Update tracking file " + trackingFile.getAbsolutePath() + " is not well formatted XML document.", e);
            }
        } catch ( java.io.IOException e ) {
            e.printStackTrace();
            if (trackingFile == null) {
                throw new BuildException ("I/O error while accessing tracking data in InputStream", e);
            } else {
                throw new BuildException ("I/O error while accessing tracking file " + trackingFile.getAbsolutePath(), e);
            }
        }
            
        Element element = document.getDocumentElement();
        if ((element != null) && element.getTagName().equals(ELEMENT_MODULE)) {
            scanElement_module(element);
        }
    }    
    
    /** Scan through Element named module. */
    void scanElement_module(Element element) { // <module>
        module = new Module();        
        NamedNodeMap attrs = element.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            Attr attr = (Attr) attrs.item(i);
            if (attr.getName().equals(ATTR_CODENAME)) { // <module codename="???">
                module.setCodename( attr.getValue() );
            }
        }
        NodeList nodes = element.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if ( node.getNodeType() == Node.ELEMENT_NODE ) {
                Element nodeElement = (Element) node;
                if (nodeElement.getTagName().equals(ELEMENT_VERSION)) {
                    scanElement_module_version(nodeElement, module);
                }
            }
        }
    }
    
    /** Scan through Element named module_version. */
    void scanElement_module_version(Element element, Module module) { // <module_version>
        Version version = new Version();        
        NamedNodeMap attrs = element.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            Attr attr = (Attr) attrs.item(i);
            if (attr.getName().equals(ATTR_VERSION)) { // <module_version specification_version="???">
                version.setVersion( attr.getValue() );
            }
            if (attr.getName().equals(ATTR_ORIGIN)) { // <module_version origin="???">
                version.setOrigin( attr.getValue() );
            }
            if (attr.getName().equals(ATTR_LAST)) { // <module_version last="???">                
                version.setLast( Boolean.getBoolean(attr.getValue() ));
            }
            if (attr.getName().equals(ATTR_INSTALL)) { // <module_version install_time="???">
                long li = 0;
                try {
                    li = Long.parseLong( attr.getValue() );
                } catch ( NumberFormatException nfe ) {
                }
                version.setInstall_time( li );
            }
        }
        NodeList nodes = element.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element nodeElement = (Element) node;
                if (nodeElement.getTagName().equals(ELEMENT_FILE)) {
                    scanElement_file(nodeElement, version);
                }
            }
        }
        module.addVersion( version );
    }
    
    /** Scan through Element named file. */
    void scanElement_file(Element element, Version version) { // <file>
        ModuleFile file = new ModuleFile();        
        NamedNodeMap attrs = element.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            Attr attr = (Attr)attrs.item(i);
            if (attr.getName().equals(ATTR_FILE_NAME)) { // <file name="???">
                file.setName( attr.getValue().replace(File.separatorChar,'/') );
            }
            if (attr.getName().equals(ATTR_CRC)) { // <file crc="???">
                file.setCrc( attr.getValue() );
            }
        }
        version.addFile (file );
    }

    static CRC32 crcForFile(File inFile) throws FileNotFoundException, IOException {
        try (FileInputStream inFileStream = new FileInputStream(inFile)) {
            byte[] array = new byte[(int) inFile.length()];
            CRC32 crc = new CRC32();
            int len = inFileStream.read(array);
            if (len != array.length) {
                throw new BuildException("Cannot fully read " + inFile);
            }
            crc.update(array);
            return crc;
        }
    }
    
    class Module extends Object {        
        
        /** Holds value of property codename. */
        private String codename;
        
        /** Holds value of property versions. */
        private List<Version> versions = new ArrayList<>();
        
        /** Getter for property codenamebase.
         * @return Value of property codenamebase.
         */
        String getCodenamebase() {
	    String codenamebase = new String(codename);
            int idx = codenamebase.lastIndexOf ('/'); //NOI18N
            if (idx != -1) codenamebase = codenamebase.substring (0, idx);

            return codenamebase;
        }

         /** Getter for property codename.
         * @return Value of property codename.
         */
        String getCodename() {
            return codename;
        }
       
        /** Setter for property codename.
         * @param codename New value of property codename.
         */
        void setCodename(String codename) {
            this.codename = codename;
        }
        
        /** Getter for property versions.
         * @return Value of property versions.
         */
        List<Version> getVersions() {
            return versions;
        }
        
        /** Setter for property versions.
         * @param versions New value of property versions.
         */
        void setVersions(List<Version> versions) {
            this.versions = versions;
        }
        
        void addVersion( Version version ) {
            versions = new ArrayList<>();
            versions.add( version );
        }

        void setVersion( Version version ) {
            versions = new ArrayList<>();
            versions.add( version );
        }
        
        void removeLocalized( String locale ) {
            for(Version ver: versions) {
                ver.removeLocalized( locale );
            }
        }
    }
    
    public class Version extends Object {        
        
        /** Holds value of property version. */
        private String version;
        
        /** Holds value of property origin. */
        private String origin;
        
        /** Holds value of property last. */
        private boolean last;
        
        /** Holds value of property install_time. */
        private long install_time = 0;
        
        /** Holds value of property files. */
        private List<ModuleFile> files = new ArrayList<>();
        
        /** Getter for property version.
         * @return Value of property version.
         */
        String getVersion() {
            return version;
        }
        
        /** Setter for property version.
         * @param version New value of property version.
         */
        void setVersion(String version) {
            this.version = version;
        }
        
        /** Getter for property origin.
         * @return Value of property origin.
         */
        String getOrigin() {
            return origin;
        }
        
        /** Setter for property origin.
         * @param origin New value of property origin.
         */
        void setOrigin(String origin) {
            this.origin = origin;
        }
        
        /** Getter for property last.
         * @return Value of property last.
         */
        boolean isLast() {
            return last;
        }
        
        /** Setter for property last.
         * @param last New value of property last.
         */
        void setLast(boolean last) {
            this.last = last;
        }
        
        /** Getter for property install_time.
         * @return Value of property install_time.
         */
        long getInstall_time() {
            return install_time;
        }
        
        /** Setter for property install_time.
         * @param install_time New value of property install_time.
         */
        void setInstall_time(long install_time) {
            this.install_time = install_time;
        }
        
        /** Getter for property files.
         * @return Value of property files.
         */
        List<ModuleFile> getFiles() {
            return files;
        }
        
        /** Setter for property files.
         * @param files New value of property files.
         */
        void setFiles(List<ModuleFile> files) {
            this.files = files;
        }
        
        void addFile( ModuleFile file ) {
            files.add( file );
        }
        
        public void addFileWithCrc( String filename, String crc ) {
            ModuleFile file = new ModuleFile();
            file.setName( filename );
            file.setCrc( crc);
            files.add( file );
        }
        
        public void removeLocalized( String locale ) {
            List<ModuleFile> newFiles = new ArrayList<>();
            for (ModuleFile file : files) {
                if (file.getName().indexOf("_" + locale + ".") == -1 // NOI18N
                        && file.getName().indexOf("_" + locale + "/") == -1 // NOI18N
                        && !file.getName().endsWith("_" + locale) ) // NOI18N
                    newFiles.add ( file );
            }
            files = newFiles;
            
        }

        void addFileForRoot(File file) throws IOException {
            CRC32 crc = crcForFile(file);
            if (!file.getPath().startsWith(nbPath)) {
                throw new BuildException("File " + file + " needs to be under " + nbPath);
            }
            String rel = file.getPath().substring(nbPath.length()).replace(File.separatorChar, '/');
            if (rel.startsWith("/")) {
                rel = rel.substring(1);
            }
            addFileWithCrc(rel, "" + crc.getValue());
        }
    }
    
    class ModuleFile extends Object {        
        
        /** Holds value of property name. */
        private String name;
        
        /** Holds value of property crc. */
        private String crc;
        
        /** Getter for property name.
         * @return Value of property name.
         */
        String getName() {
            return name;
        }
        
        /** Setter for property name.
         * @param name New value of property name.
         */
        void setName(String name) {
            this.name = name.replace(File.separatorChar,'/');
        }
        
        /** Getter for property crc.
         * @return Value of property crc.
         */
        String getCrc() {
            return crc;
        }
        
        /** Setter for property crc.
         * @param crc New value of property crc.
         */
        void setCrc(String crc) {
            this.crc = crc;
        }
        
    }

}
