/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (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.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * 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 com.queplix.core.modules.web;

import com.queplix.core.modules.web.jxb.Exclude;
import com.queplix.core.modules.web.jxb.Include;
import com.queplix.core.modules.web.jxb.JsGroup;
import com.queplix.core.modules.web.jxb.JsGroups;
import com.queplix.core.utils.FileHelper;
import com.queplix.core.utils.xml.XMLBinding;
import com.queplix.core.utils.xml.XMLFactory;
import com.queplix.core.utils.xml.XMLWrapper;
import org.apache.regexp.RESyntaxException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.jresearch.jssplitter.jsprocessor.DescBean;
import org.jresearch.jssplitter.jsprocessor.JSFileProcessor;
import org.w3c.dom.Document;

import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * <p>Java Script compiler ANT task</p>
 * <p>Compiles JS files using Java Script Groups (JSG) XML configs.</p>
 *
 * @author [ALB] Baranov Andrey
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:31:03 $
 */

public final class JSCompiler
        extends Task {

    // ----------------------------------------------------- constants

    private static final String JSG_CONFIG_FILES_EXTENSION = "xml";

    // ----------------------------------------------------- variables

    private String jsgDir;
    private String inDir;
    private String outDir;

    private File jsgDirFile;
    private File inDirFile;
    private File outDirFile;

    private XMLWrapper xmlWrapper;
    private XMLBinding xmlBind;

    private Map jsgMap;

    // ----------------------------------------------------- public methods

    //
    // Setters.
    //

    public void setJsgDir(String s) {
        if(s == null) {
            throw new BuildException("JS group config dir is NULL");
        }
        jsgDir = s;
    }

    public void setInDir(String s) {
        if(s == null) {
            throw new BuildException("JS input dir is NULL");
        }
        inDir = s;
    }

    public void setOutDir(String s) {
        if(s == null) {
            throw new BuildException("JS output dir is NULL");
        }
        outDir = s;
    }

    /* (non-Javadoc)
     * @see org.apache.tools.ant.Task#execute()
     */
    public void execute()
            throws BuildException {

        // Init.
        jsgDirFile = new File(getProject().getBaseDir(), jsgDir);
        if(!jsgDirFile.exists() || !jsgDirFile.isDirectory()) {
            throw new BuildException(
                    "Bad JSG dir: " + jsgDirFile.getAbsolutePath());
        }
        inDirFile = new File(getProject().getBaseDir(), inDir);
        if(!inDirFile.exists() || !inDirFile.isDirectory()) {
            throw new BuildException(
                    "Bad JS in dir: " + outDirFile.getAbsolutePath());
        }
        outDirFile = new File(getProject().getBaseDir(), outDir);
        if(!outDirFile.exists()) {
            // create new dir
            outDirFile.mkdirs();
        }

        xmlWrapper = XMLFactory.getXMLWrapper();
        xmlBind = XMLFactory.getXMLBinding();

        jsgMap = new HashMap();
        initJSGConfigs();

        // Compile JS files.
        List browserTypes = WebHelper.SCRIPT_SUPPORTED_TYPES;
        int size = (browserTypes == null) ? 0:browserTypes.size();
        if(size == 0) {
            throw new BuildException("No any supported browsers found");
        }

        for(int i = 0; i < size; i++) {
            compileFiles((String) browserTypes.get(i));
        }
    }

    /* (non-Javadoc)
     * @see org.apache.tools.ant.Task#init()
     */
    public void init() {
        super.init();
    }

    /* (non-Javadoc)
     * @see org.apache.tools.ant.ProjectComponent#log(java.lang.String)
     */
    public void log(String s) {
        super.log(s);
    }

    // ----------------------------------------------------- private methods

    //
    // Init <code>jsgMap</code> map.
    //

    private void initJSGConfigs() {

        // get list of files
        File[] files = jsgDirFile.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                String ext = JSG_CONFIG_FILES_EXTENSION;
                if(name.substring(name.length() - ext.length()).equals(ext)) {
                    return true;
                } else {
                    return false;
                }
            }
        });
        if(files.length == 0) {
            throw new BuildException("Can't find any JSG configs in path '" +
                    jsgDirFile.getAbsolutePath() + "'");
        }

        // cycle read JSG config XML files
        for(int i = 0; i < files.length; i++) {
            JsGroup[] jsgArray = parseJSGConfig(files[i]);
            int size = (jsgArray == null) ? 0:jsgArray.length;

            for(int j = 0; j < size; j++) {
                JsGroup jsg = jsgArray[j];
                jsgMap.put(jsg.getName(), jsg);
            }
        }
    }

    //
    // Read one JSG config file.
    //
    private JsGroup[] parseJSGConfig(File file) {

        String fileName = file.getName();
        JsGroups jsgConfig = null;

        try {
            // read entity config XML file
            log("  Read from " + file.getCanonicalPath());

            // build JsGroups object
            Document document = xmlWrapper.getDocument(file, false);
            jsgConfig = (JsGroups) xmlBind.xmlToJava(JsGroups.class, document);

        } catch (IOException ex) {
            throw new BuildException(
                    "IO exception. Can't process parsing JSG from file '" +
                            fileName + "': " + ex.getMessage(), ex);
        }

        return jsgConfig.getJsGroup();
    }

    //
    // Compile JS files.
    //
    private void compileFiles(String browserType) {

        for(Iterator it = jsgMap.keySet().iterator(); it.hasNext();) {
            String jsgName = (String) it.next();
            JsGroup jsg = (JsGroup) jsgMap.get(jsgName);
            boolean doCompile = jsg.getCompile().booleanValue();

            // .. get list of file names for given JSG
            File[] files = getJSFiles(jsg, browserType);
            int size = (files == null) ? 0:files.length;
            String[] fileNames = new String[size];
            try {
                for(int i = 0; i < size; i++) {
                    fileNames[i] = files[i].getCanonicalPath();
                }
            } catch (IOException ex) {
                throw new BuildException("IO exception: " + ex.getMessage(),
                        ex);
            }

            if(doCompile) {

                // .. get target and index files and create subfolder for them
                File targetFile = WebHelper.getJSPath(jsgName, browserType,
                        outDirFile.getAbsolutePath());
                File indexFile = WebHelper.getJSIndexPath(jsgName, browserType,
                        outDirFile.getAbsolutePath());
                targetFile.getParentFile().mkdirs();

                log("	Compile JS group: " + jsgName + ", files: " + size);
                log("		to: " + targetFile);
                log("		index file: " + indexFile);

                // .. call JSFileProcessor#makeTarget method of jssplitter library.
                try {
                    DescBean desc = new DescBean(fileNames,
                            targetFile.getAbsolutePath(), true);
                    JSFileProcessor.makeTarget(desc);

                } catch (IOException ex) {
                    throw new BuildException("IO exception: " + ex.getMessage(),
                            ex);

                } catch (RESyntaxException ex) {
                    throw new BuildException(
                            "Regexp exception: " + ex.getMessage(), ex);
                }

                // .. generate index file.
                try {
                    PrintWriter pw = new PrintWriter(new FileWriter(indexFile));
                    for(int i = 0; i < size; i++) {
                        File file = files[i];
                        String url = WebHelper.getJSURL(file, outDirFile);
                        pw.println(url);
                    }
                    pw.flush();
                    pw.close();

                } catch (IOException ex) {
                    throw new BuildException("IO exception: " + ex.getMessage(),
                            ex);
                }

            } else {
                log("	Create JS group: " + jsgName + ", files: " + size);

                // .. copy files to the target directory
                for(int i = 0; i < size; i++) {
                    File src = files[i];
                    File dest = WebHelper.getNCJSPath(jsgName, src, outDirFile);

                    // ... create target directory
                    dest.getParentFile().mkdirs();

                    // ... do copy
                    copyFiles(src, dest);
                }
            }
        }
    }

    /**
     * Get array of JS files for given <code>jsg</code> JS group.
     *
     * @param jsg         JS group
     * @param browserType browser type parameter
     * @return array of JS files or NULL
     */
    private File[] getJSFiles(JsGroup jsg, String browserType) {
        List ret = new ArrayList();
        getJSFiles(jsg, browserType, ret);
        return (ret.size() == 0) ? null:(File[]) ret.toArray(new File[0]);
    }

    // @see #getJSFiles( JsGroup, String )
    private void getJSFiles(JsGroup jsg, String browserType, List ret) {

        Include[] includes = jsg.getInclude();
        Exclude[] excludes = jsg.getExclude();

        // 1. Add includes.
        if(includes != null) {
            for(int i = 0; i < includes.length; i++) {
                Include include = includes[i];
                String src = include.getSrc();
                String ref = include.getRef();

                if(src != null) {
                    // 1.1 Add file object.
                    log("### set includes: " + src);

                    // .. add file(s)
                    String[] fileNames = searchFiles(src);
                    int size = (fileNames == null) ? 0:fileNames.length;
                    for(int j = 0; j < size; j++) {
                        File file = new File(inDirFile, fileNames[j]);
                        __addFile(file, ret);

                        // .. add implemenation if exist
                        File implFile = getImplJSFile(file, browserType);
                        if(implFile != null) {
                            __addFile(implFile, ret);
                        }
                    }

                } else if(ref != null) {
                    // 1.2 Add ref object.
                    log("### set ref: " + ref);

                    JsGroup refJsg = (jsgMap == null) ? null
                            :((JsGroup) jsgMap.get(ref));
                    if(refJsg == null) {
                        throw new BuildException(
                                "JSG '" + ref + "' not found.");
                    }

                    // .. add content of ref group
                    // WARNING: can be loop
                    getJSFiles(refJsg, browserType, ret);
                }
            }
        }

        // 2. Remove excludes.
        if(excludes != null) {
            for(int i = 0; i < excludes.length; i++) {
                Exclude exclude = excludes[i];
                String src = exclude.getSrc();

                log("### set excludes: " + src);

                // .. remove file(s)
                String[] fileNames = searchFiles(src);
                int size = (fileNames == null) ? 0:fileNames.length;
                for(int j = 0; j < size; j++) {
                    File file = new File(inDirFile, fileNames[j]);
                    __removeFile(file, ret);

                    // .. remove implemenation if exist
                    File implFile = getImplJSFile(file, browserType);
                    if(implFile != null) {
                        __removeFile(implFile, ret);
                    }
                }
            }
        }
    }

    // Search files by the mask <code>src</code>.
    private String[] searchFiles(String src) {
        FileSet fileset = new FileSet();
        fileset.setDir(inDirFile);
        fileset.setIncludes(src);
        DirectoryScanner dirscan = fileset.getDirectoryScanner(getProject());
        return dirscan.getIncludedFiles();
    }

    // Get implementation for <code>jsFile</code> or NULL.
    private File getImplJSFile(File jsFile, String browserType) {
        File implFile = WebHelper.getImplementation(jsFile, browserType);
        if(implFile.exists() && WebHelper.isJSFile(implFile)) {
            return implFile;
        } else {
            return null;
        }
    }

    // Copy file <code>src</code> to <code>dest</code>.
    private void copyFiles(File src, File dest) {
        try {
            FileHelper.copyFile(src, dest);
        } catch (IOException ex) {
            throw new BuildException("IO exception: " + ex.getMessage(), ex);
        }
    }

    // Add file in list.
    private void __addFile(File f, List l) {
        if(!l.contains(f)) {
            log(" 	add file: " + f);
            l.add(f);
        }
    }

    // Remove file from list.
    private void __removeFile(File f, List l) {
        if(l.contains(f)) {
            log(" 	remove file: " + f);
            l.remove(f);
        }
    }
}
