package act.sys;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * Licensed 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.
 * #L%
 */

import act.Act;
import act.asm.Type;
import org.osgl.util.C;
import org.osgl.util.OS;
import org.osgl.util.S;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.annotation.*;
import java.lang.management.ManagementFactory;
import java.lang.reflect.AnnotatedElement;

/**
 * Mark a module should only be loaded in certain environment
 */
public final class Env {

    private Env() {}

    /**
     * Used to mark a dependency injector module that
     * should be load only in specified profile.
     *
     * This annotation shall NOT used along with
     * {@link Mode} and {@link Group}
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequireProfile {

        /**
         * The profile specification
         */
        String value();

        /**
         * If `except` is `true` then the module should be load
         * when the current profile is **NOT** the value specified
         */
        boolean except() default false;
    }

    /**
     * Used to mark a dependency injector module that
     * should be load only in specified profile.
     *
     * This annotation shall NOT used along with
     * {@link Mode} and {@link Group}
     *
     * This annotation is deprecated. Please use {@link RequireProfile} instead
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Deprecated
    public @interface Profile {

        /**
         * The profile specification
         */
        String value();

        /**
         * If unless is `true` then the module should be load
         * unless the current profile is the value specified
         */
        boolean unless() default false;
    }

    /**
     * Used to mark a dependency injector module that
     * should be load only in specified node group
     *
     * This annotation shall NOT used along with
     * {@link Mode} and {@link RequireProfile}
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequireGroup {

        /**
         * The node group specification
         */
        String value();

        /**
         * If `except` is `true` then the module should be load
         * when the current node group is **NOT** the value specified
         */
        boolean except() default false;
    }

    /**
     * Used to mark a dependency injector module that
     * should be load only in specified node group
     *
     * This annotation shall NOT used along with
     * {@link Mode} and {@link Profile}
     *
     * This annotation is deprecated. Please use {@link RequireGroup}
     * instead
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Deprecated
    public @interface Group {

        /**
         * The node group specification
         */
        String value();

        /**
         * If unless is `true` then the module should be load
         * unless the current node group is the value specified
         */
        boolean unless() default false;
    }

    /**
     * Used to mark a dependency injector module
     * that should be load only in specified mode
     *
     * This annotation shall NOT used along with
     * {@link RequireProfile} and {@link RequireGroup}
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequireMode {

        /**
         * The mode specification
         */
        Act.Mode value();

        /**
         * If `except` is `true` then the module should be load
         * when the current mode is **NOT** the value specified
         */
        boolean except() default false;
    }

    /**
     * Used to mark a dependency injector module
     * that should be load only in specified mode
     *
     * This annotation shall NOT used along with
     * {@link RequireProfile} and {@link RequireGroup}
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Mode {

        /**
         * The mode specification
         */
        Act.Mode value();

        /**
         * If unless is `true` then the module should be load
         * unless the current mode is the value specified
         */
        boolean unless() default false;
    }

    public static boolean matches(RequireMode modeTag) {
        return modeMatches(modeTag.value(), modeTag.except());
    }

    /**
     * This method is deprecated. Please use {@link #matches(RequireMode)} instead
     */
    @Deprecated
    public static boolean matches(Mode modeTag) {
        return modeMatches(modeTag.value(), modeTag.unless());
    }

    public static boolean modeMatches(Act.Mode mode) {
        return mode == Act.mode();
    }

    public static boolean modeMatches(Act.Mode mode, boolean unless) {
        return unless ^ modeMatches(mode);
    }

    public static boolean matches(RequireProfile profileTag) {
        return profileMatches(profileTag.value(), profileTag.except());
    }

    /**
     * This method is deprecated. Please use {@link #matches(RequireProfile)} instead
     */
    @Deprecated
    public static boolean matches(Profile profileTag) {
        return profileMatches(profileTag.value(), profileTag.unless());
    }

    public static boolean profileMatches(String profile) {
        return S.eq(profile, Act.profile(), S.IGNORECASE);
    }

    public static boolean profileMatches(String profile, boolean unless) {
        return unless ^ profileMatches(profile);
    }

    public static boolean modeMatches(String mode) {
        return S.eq(mode, Act.mode().name(), S.IGNORECASE);
    }

    public static boolean modeMatches(String mode, boolean unless) {
        return unless ^ modeMatches(mode);
    }

    public static boolean matches(RequireGroup groupTag) {
        return groupMatches(groupTag.value(), groupTag.except());
    }

    /**
     * This method is deprecated. Please use {@link #matches(RequireGroup)} instead
     */
    @Deprecated
    public static boolean matches(Group groupTag) {
        return groupMatches(groupTag.value(), groupTag.unless());
    }

    public static boolean groupMatches(String group) {
        return S.eq(group, Act.nodeGroup(), S.IGNORECASE);
    }

    public static boolean groupMatches(String group, boolean unless) {
        return unless ^ groupMatches(group);
    }

    private static final C.Set<Class<? extends Annotation>> ENV_ANNOTATION_TYPES = C.set(
            Env.Mode.class, Env.Profile.class, Env.Group.class,
            Env.RequireProfile.class, Env.RequireGroup.class, Env.RequireMode.class
    );

    private static final C.Set<String> ENV_ANNO_DESCS = C.set(
            Type.getType(Mode.class).getDescriptor(),
            Type.getType(RequireMode.class).getDescriptor(),
            Type.getType(Profile.class).getDescriptor(),
            Type.getType(RequireProfile.class).getDescriptor(),
            Type.getType(Group.class).getDescriptor(),
            Type.getType(RequireGroup.class).getDescriptor()
    );

    public static boolean isEnvAnnotation(Class<? extends Annotation> type) {
        return ENV_ANNOTATION_TYPES.contains(type);
    }

    public static boolean isEnvAnnoDescriptor(String descriptor) {
        return ENV_ANNO_DESCS.contains(descriptor);
    }

    /**
     * Determine if an {@link AnnotatedElement} has environment annotations and if it has then check
     * if all environment annotations matches the current executing environment
     * @param annotatedElement an annotated element
     * @return `true` if the element does not have environment annotations or all environment annotations matches
     */
    public static boolean matches(AnnotatedElement annotatedElement) {
        Annotation[] annotations = annotatedElement.getDeclaredAnnotations();
        for (Annotation anno : annotations) {
            if (anno instanceof RequireProfile) {
                RequireProfile profile = (RequireProfile) anno;
                if (!matches(profile)) {
                    return false;
                }
            } else if (anno instanceof RequireMode) {
                RequireMode mode = (RequireMode) anno;
                if (!matches(mode)) {
                    return false;
                }
            } else if (anno instanceof RequireGroup) {
                RequireGroup group = (RequireGroup) anno;
                if (!matches(group)) {
                    return false;
                }
            } else if (anno instanceof Profile) {
                Profile profile = (Profile)anno;
                if (!matches(profile)) {
                    return false;
                }
            } else if (anno instanceof Group) {
                Group group = (Group) anno;
                if (!matches(group)) {
                    return false;
                }
            } else if (anno instanceof Mode) {
                Mode mode = (Mode) anno;
                if (!matches(mode)) {
                    return false;
                }
            }
        }
        return true;
    }

    // See http://stackoverflow.com/questions/534648/how-to-daemonize-a-java-program
    public static class PID {

        private static String pid = getPid();

        private static String getPid() {
            OS os = OS.get();
            if (os.isUnix()) {
                File proc_self = new File("/proc/self");
                if(proc_self.exists()) try {
                    return proc_self.getCanonicalFile().getName();
                }
                catch(Exception e) {
                    /// Continue on fall-back
                }
                File bash = new File("/bin/sh");
                if(bash.exists()) {
                    ProcessBuilder pb = new ProcessBuilder("/bin/sh","-c","echo $PPID");
                    try {
                        Process p = pb.start();
                        BufferedReader rd = new BufferedReader(new InputStreamReader(p.getInputStream()));
                        return rd.readLine();
                    }
                    catch(IOException e) {
                        return String.valueOf(Thread.currentThread().getId());
                    }
                }
            } else {
                String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
                if (null != nameOfRunningVM) {
                    int p = nameOfRunningVM.indexOf('@');
                    if (p > -1) {
                        return nameOfRunningVM.substring(0, p);
                    }
                }
            }
            // The final resort
            return String.valueOf(Thread.currentThread().getId());
        }

        public static String get() {
            return pid;
        }
    }

}
