/*
 * 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.modules.java.j2sedeploy;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32;
import org.netbeans.api.actions.Closable;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ant.AntBuildExtender;
import org.netbeans.modules.java.j2seproject.api.J2SEPropertyEvaluator;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.util.Lookup;
import org.openide.util.Mutex;
import org.openide.util.MutexException;
import org.openide.util.Parameters;

/**
 *  Java SE Deployment panel Project Properties support
 * 
 * @author Petr Somol
 * @author Tomas Zezula
 * @since java.j2seproject 1.65
 */
public final class J2SEDeployProperties {

    // Deployment - native packaging
    public static final String NATIVE_BUNDLING_ENABLED = "native.bundling.enabled"; //NOI18N
    // copied from JFXProjectProperties
    public static final String JAVAFX_ENABLED = "javafx.enabled"; // NOI18N
    private static final String J2SEDEPLOY_EXTENSION = "j2sedeploy";    //NOI18N
    private static final String[] OLD_J2SEDEPLOY_EXTENSION = new String[0];
    private static final String BUILD_SCRIPT_PROTOTYPE = String.format(
        "%s/resources/build-native-prototype.xml",  //NOI18N
        J2SEDeployProperties.class.getPackage().getName().replace('.','/'));   //NOI18N
    private static final String EXTENSION_BUILD_SCRIPT_PATH = "nbproject/build-native.xml";        //NOI18N

    private static final Logger LOG = Logger.getLogger(J2SEDeployProperties.class.getName());
    
    // Project related references
    private J2SEPropertyEvaluator j2sePropEval;
    private PropertyEvaluator evaluator;
    private Project project;

    public Project getProject() {
        return project;
    }
    public PropertyEvaluator getEvaluator() {
        return evaluator;
    }
    
    boolean nativeBundlingEnabled;
    public boolean getNativeBundlingEnabled() {
        return nativeBundlingEnabled;
    }
    public void setNativeBundlingEnabled(boolean enabled) {
        this.nativeBundlingEnabled = enabled;
    }

    /** Keeps singleton instance of J2SEDeployProperties for SE project with Deployment category open */
    private static Map<String, J2SEDeployProperties> propInstance = new HashMap<String, J2SEDeployProperties>();

    /** Factory method */
    public static J2SEDeployProperties getInstance(Lookup context) {
        Project proj = context.lookup(Project.class);
        String projDir = proj.getProjectDirectory().getPath();
        J2SEDeployProperties prop = propInstance.get(projDir);
        if(prop == null) {
            prop = new J2SEDeployProperties(context);
            propInstance.put(projDir, prop);
        }
        return prop;
    }

    /** Getter method */
    public static J2SEDeployProperties getInstanceIfExists(Project proj) {
        assert proj != null;
        String projDir = proj.getProjectDirectory().getPath();
        J2SEDeployProperties prop = propInstance.get(projDir);
        if(prop != null) {
            return prop;
        }
        return null;
    }

    /** Getter method */
    public static J2SEDeployProperties getInstanceIfExists(Lookup context) {
        Project proj = context.lookup(Project.class);
        return getInstanceIfExists(proj);
    }

    public static void cleanup(Lookup context) {
        Project proj = context.lookup(Project.class);
        String projDir = proj.getProjectDirectory().getPath();
        propInstance.remove(projDir);
    }

    static String getCurrentExtensionName() {
        return J2SEDEPLOY_EXTENSION;
    }

    static String[] getOldExtensionNames() {
        return OLD_J2SEDEPLOY_EXTENSION;
    }

    static void updateJ2SEDeployExtension(@NonNull final Project project) throws IOException {
        Parameters.notNull("project", project); //NOI18N
        final FileObject buildNativeXml = copyBuildNativeTemplate(project);
        assert buildNativeXml != null;
        final AntBuildExtender extender = project.getLookup().lookup(AntBuildExtender.class);
        assert extender != null;
        modifyBuildXml(extender, buildNativeXml);
    }

    static boolean isBuildNativeUpToDate(@NonNull final Project project) {
        Parameters.notNull("project", project); //NOI18N
        final FileObject buildNativeXml = project.getProjectDirectory().getFileObject(EXTENSION_BUILD_SCRIPT_PATH);
        if (buildNativeXml == null) {
            return false;
        }
        try (
            final InputStream buildIn = new BufferedInputStream(buildNativeXml.getInputStream());
            final InputStream protoIn = new BufferedInputStream(J2SEDeployProperties.class.getClassLoader().getResourceAsStream(BUILD_SCRIPT_PROTOTYPE))) {
            final long buildNativeCRC = computeCRC(buildIn);
            final long prototypeCRC = computeCRC(protoIn);
            return buildNativeCRC == prototypeCRC;
        } catch (IOException ioe) {
            LOG.log(
                Level.INFO,
                "Cannot read: {0}", //NOI18N
                FileUtil.getFileDisplayName(buildNativeXml));
            return false;
        }
    }

    static FileObject copyBuildNativeTemplate(@NonNull final Project project) throws IOException {
        Parameters.notNull("project", project); //NOI18N
        final FileObject buildExFoBack = project.getProjectDirectory().getFileObject(String.format(
            "%s~",  //NOI18N
            EXTENSION_BUILD_SCRIPT_PATH));
        if (buildExFoBack != null) {
            closeInEditor(buildExFoBack);
            buildExFoBack.delete();
        }
        FileObject buildExFo = project.getProjectDirectory().getFileObject(EXTENSION_BUILD_SCRIPT_PATH);
        FileLock lock;
        if (buildExFo != null) {
            closeInEditor(buildExFo);
            lock = buildExFo.lock();
            try {
                buildExFo.rename(
                    lock,
                    buildExFo.getName(),
                    String.format(
                        "%s~",  //NOI18N
                        buildExFo.getExt()));
            } finally {
                lock.releaseLock();
            }
        }
        buildExFo = FileUtil.createData(project.getProjectDirectory(), EXTENSION_BUILD_SCRIPT_PATH);
        lock = buildExFo.lock();
        try (final InputStream in = J2SEDeployProperties.class.getClassLoader().getResourceAsStream(BUILD_SCRIPT_PROTOTYPE);
             final OutputStream out = buildExFo.getOutputStream(lock)) {
            FileUtil.copy(in, out);
        } finally {
            lock.releaseLock();
        }
        return buildExFo;
    }

    private static AntBuildExtender.Extension modifyBuildXml(
        @NonNull final AntBuildExtender extender,
        @NonNull final FileObject buildNativeXml) {
        Parameters.notNull("extender", extender);   //NOI18N
        Parameters.notNull("buildNativeXml", buildNativeXml);   //NOI18N
        return extender.addExtension(getCurrentExtensionName(), buildNativeXml);
    }

    private static void closeInEditor(@NonNull final FileObject file) {
        Parameters.notNull("file", file);   //NOI18N
        try {
            final DataObject dobj = DataObject.find(file);
            final Closable closeCookie = dobj.getLookup().lookup(Closable.class);
            if (closeCookie != null) {
                closeCookie.close();
            }
        } catch (DataObjectNotFoundException donfe) {
            LOG.log(
                Level.INFO,
                "Cannot close {0}.", //NOI18N
                FileUtil.getFileDisplayName(file));
        }
    }

    private static long computeCRC(@NonNull final InputStream in) throws IOException {
        final CRC32 crc = new CRC32();
        int last = -1;
        int curr;
        while ((curr = in.read()) != -1) {
            if (curr != '\n' && last == '\r') { //NOI18N
                crc.update('\n');               //NOI18N
            }
            if (curr != '\r') {                 //NOI18N
                crc.update(curr);
            }
            last = curr;
        }
        if (last == '\r') {                     //NOI18N
            crc.update('\n');                   //NOI18N
        }        
        return crc.getValue();
    }

    /** Creates a new instance of J2SEDeployProperties */
    private J2SEDeployProperties(Lookup context) {       
        project = context.lookup(Project.class);
        if (project != null) {
            j2sePropEval = project.getLookup().lookup(J2SEPropertyEvaluator.class);
            evaluator = j2sePropEval.evaluator();
            nativeBundlingEnabled = isTrue(evaluator.getProperty(NATIVE_BUNDLING_ENABLED));
        }
    }
    
    private static void setOrRemove(@NonNull org.netbeans.spi.project.support.ant.EditableProperties props, @NonNull String name, String value) {
        if (value != null) {
            props.setProperty(name, value);
        } else {
            props.remove(name);
        }
    }
  
    public void store() throws IOException {
        final EditableProperties ep = new EditableProperties(true);
        final FileObject projPropsFO = project.getProjectDirectory().getFileObject(AntProjectHelper.PROJECT_PROPERTIES_PATH);
        try {
            ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
                @Override
                public Void run() throws Exception {
                    try (final InputStream is = projPropsFO.getInputStream()) {
                        ep.load(is);
                    }
                    setOrRemove(ep, NATIVE_BUNDLING_ENABLED, nativeBundlingEnabled ? "true" : null); //NOI18N
                    FileLock lock = projPropsFO.lock();
                    try (OutputStream os = projPropsFO.getOutputStream(lock)) {
                        ep.store(os);
                    } finally {
                        lock.releaseLock();
                    }
                    final AntBuildExtender extender = project.getLookup().lookup(AntBuildExtender.class);
                    if (extender != null) {
                        AntBuildExtender.Extension extension = extender.getExtension(getCurrentExtensionName());
                        if (nativeBundlingEnabled) {
                            if (extension == null) {
                                final FileObject buildExFo = copyBuildNativeTemplate(project);
                                extension = modifyBuildXml(extender, buildExFo);
                            }
                        } else {
                            if (extension != null) {
                                extender.removeExtension(getCurrentExtensionName());
                            }
                            final FileObject buildExFo = project.getProjectDirectory().getFileObject(EXTENSION_BUILD_SCRIPT_PATH);
                            if (buildExFo != null) {
                                closeInEditor(buildExFo);
                                buildExFo.delete();
                            }
                        }
                    }
                    return null;
                }
            });
        } catch (MutexException mux) {
            throw (IOException) mux.getException();
        }
    }
    
    public static boolean isTrue(final String value) {
        return value != null &&
                (value.equalsIgnoreCase("true") ||  //NOI18N
                 value.equalsIgnoreCase("yes") ||   //NOI18N
                 value.equalsIgnoreCase("on"));     //NOI18N
    }

    public static boolean isNonEmpty(String s) {
        return s != null && !s.isEmpty();
    }
            
    public static boolean isEqual(final String s1, final String s2) {
        return (s1 == null && s2 == null) ||
                (s1 != null && s2 != null && s1.equals(s2));
    }                                   

    public static boolean isEqualIgnoreCase(final String s1, final String s2) {
        return (s1 == null && s2 == null) ||
                (s1 != null && s2 != null && s1.equalsIgnoreCase(s2));
    }                                   

    public static boolean isEqualText(final String s1, final String s2) {
        return ((s1 == null || s1.isEmpty()) && (s2 == null || s2.isEmpty())) ||
                (s1 != null && s2 != null && s1.equals(s2));
    }                                   

}
