/*
 * Sakuli - Testing and Monitoring-Tool for Websites and common UIs.
 *
 * Copyright 2013 - 2015 the original author or authors.
 *
 * 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.
 */

package org.sakuli.actions.environment;

import net.sf.sahi.util.OSUtils;
import org.sakuli.actions.Action;
import org.sakuli.actions.ModifySahiTimer;
import org.sakuli.actions.logging.LogToResult;
import org.sakuli.actions.screenbased.Region;
import org.sakuli.actions.screenbased.RegionImpl;
import org.sakuli.actions.screenbased.TypingUtil;
import org.sakuli.datamodel.properties.ActionProperties;
import org.sakuli.exceptions.SakuliCheckedException;
import org.sakuli.loader.BeanLoader;
import org.sakuli.loader.ScreenActionLoader;
import org.sakuli.utils.CommandLineUtil;
import org.sakuli.utils.CommandLineUtil.CommandLineResult;
import org.sakuli.utils.SakuliPropertyPlaceholderConfigurer;
import org.sikuli.script.App;
import org.sikuli.script.IRobot;
import org.sikuli.script.Key;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.event.KeyEvent;
import java.nio.file.Path;
import java.util.Properties;

import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * This is a Singeton because the Function should be stateless
 *
 * @author Tobias Schneck
 */
public class Environment implements Action {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final boolean resumeOnException;
    private ScreenActionLoader loader;
    private TypingUtil<Environment> typingUtil;

    /**
     * Creates a new Environment object, to control all non-region actions like typing or pasting.
     */
    public Environment() {
        this(false);
    }

    /**
     * Creates a new Environment object, to control all non-region actions like typing or pasting.
     *
     * @param resumeOnException if true, the test execution won't stop on an occurring error.
     */
    public Environment(boolean resumeOnException) {
        this.resumeOnException = resumeOnException;
        this.loader = BeanLoader.loadScreenActionLoader();
        this.typingUtil = new TypingUtil<>(this);
    }

    /**
     * Runs the assigned command on the host and returns the result. __Attention:__ this is OS depended feature! So be
     * aware which os you are running, maybe us to check {@link Environment#isLinux()}  or {@link Environment#isWindows()}.
     *
     * @param command OS depended command as {@link CommandLineResult}
     * @return the result as {@link String}
     * @throws SakuliCheckedException if the command won't exit with value 0
     */
    public static CommandLineResult runCommand(String command) throws SakuliCheckedException {
        return runCommand(command, true);
    }

    /**
     * Equal to {@link #runCommand(String)}, but with option to avoid throwing an exception if the exit code != 0
     */
    public static CommandLineResult runCommand(String command, boolean throwException) throws SakuliCheckedException {
        return CommandLineUtil.runCommand(command, throwException);
    }

    /**
     * set a new default similarity of the screen capturing methods. To reset the similarity call {@link
     * #resetSimilarity()}.
     *
     * @param similarity double value between 0 and 1, default = {@link ActionProperties#defaultRegionSimilarity}
     * @return this {@link Environment} or NULL on errors.
     */
    @LogToResult(message = "set similarity level", logClassInstance = false)
    public Environment setSimilarity(double similarity) {
        if (similarity >= 0 && similarity <= 1) {
            this.loader.getSettings().setMinSimilarity(similarity);
        } else {
            loader.getExceptionHandler().handleException("The similartiy must be a double value between 0 and 1!", resumeOnException);
            return null;
        }
        return this;
    }

    /**
     * Resets the current similarity of the screen capturing methods to the original default value of
     * {@link ActionProperties#defaultRegionSimilarity}.
     *
     * @return this {@link Environment} or NULL on errors.
     */
    @LogToResult(message = "reset similarity level to default")
    public Environment resetSimilarity() {
        this.loader.getSettings().restetMinSimilarity();
        return this;
    }

    /**
     * @return a {@link Region} object from the current focused window or NULL on errors.
     */
    @LogToResult(logClassInstance = false)
    public Region getRegionFromFocusedWindow() {
        org.sikuli.script.Region origRegion = App.focusedWindow();
        if (origRegion != null) {
            return new Region(origRegion, resumeOnException);
        }
        loader.getExceptionHandler().handleException("couldn't extract a Region from the current focused window", resumeOnException);
        return null;
    }

    /**
     * Takes a screenshot of the current screen and saves it to the assigned path. If there ist just a file name, the
     * screenshot will be saved in your current testcase folder.
     * Supported formats: `jpg` and `png`
     * Example:
     * ```
     * environment.takeScreenshot("test.png");
     * ```
     *
     * @param filename {@code "pathname/filename.format"} or just {@code "filename.format"}.
     * @return {@link Path} to the created screenshot OR null on errors
     */
    @LogToResult(logClassInstance = false)
    public Path takeScreenshot(final String filename) {
        return getLoader().getScreenshotActions().takeScreenshot(filename, null);
    }

    /**
     * Takes a screenshot of the current screen and add the current timestamp in the file name like e.g.:
     * ```
     * env.takeScreenshotWithTimestamp("my-screenshot");
     * ```
     * saved under:`mytestsuite/testcase1/2017_08_03_14_06_13_255_my_screenshot.png`
     *
     * @param filenamePostfix postfix for the final filename
     * @param optFolderPath   optional FolderPath, where to save the screenshot.
     *                        If null or empty: testcase folder will be used
     * @param optFormat       optional format, for the screenshot (currently supported: jpg and png)
     *                        If null or empty use property `sakuli.screenshot.format`
     * @return {@link Path} to the created screenshot OR null on errors
     */
    @LogToResult(logClassInstance = false)
    public Path takeScreenshotWithTimestamp(final String filenamePostfix, final String optFolderPath, final String optFormat) {
        return getLoader().getScreenshotActions().takeScreenshotWithTimestamp(filenamePostfix, optFolderPath, optFormat, null);
    }

    /**
     * Blocks the current testcase execution for x seconds
     *
     * @param seconds to sleep
     * @return this {@link Environment} or NULL on errors.
     */
    @LogToResult(message = "sleep and do nothing for x seconds", logClassInstance = false)
    public Environment sleep(Integer seconds) {
        return typingUtil.sleep(seconds * 1000L);
    }

    /**
     * Blocks the current testcase execution for x seconds
     *
     * @param seconds to sleep
     * @return this {@link Environment} or NULL on errors.
     */
    @LogToResult(message = "sleep and do nothing for x seconds", logClassInstance = false)
    public Environment sleep(Double seconds) {
        return typingUtil.sleep((long) (seconds * 1000L));
    }

    /**
     * Blocks the current testcase execution for x milliseconds
     *
     * @param milliseconds to sleep
     * @return this {@link Environment} or NULL on errors.
     */
    @LogToResult(message = "sleep and do nothing for x milliseconds", logClassInstance = false)
    public Environment sleepMs(Integer milliseconds) {
        return typingUtil.sleep(Long.valueOf(milliseconds));
    }

    /**
     * @return the current content of the clipboard as {@link String} or NULL on errors
     */
    @LogToResult(message = "get string from system clipboard", logClassInstance = false)
    public String getClipboard() {
        return App.getClipboard();
    }

    /**
     * sets the String parameter to the system clipboard
     *
     * @param text as {@link String}
     * @return this {@link Environment}.
     */
    @LogToResult(message = "put to clipboard", logClassInstance = false)
    public Environment setClipboard(String text) {
        App.setClipboard(text);
        return this;
    }


    /**
     * Clean the content of the clipboard.
     */
    @LogToResult(logClassInstance = false)
    public Environment cleanClipboard() {
        Application.setClipboard(" ");
        return this;
    }

    /**
     * pastes the current clipboard content into the focused area. Will do the same as "CTRL + C".
     *
     * @return this {@link Environment}.
     */
    @ModifySahiTimer
    @LogToResult(message = "paste the current clipboard into the focus", logClassInstance = false)
    public Environment pasteClipboard() {
        int mod = Key.getHotkeyModifier();
        IRobot r = loader.getScreen().getRobot();
        r.keyDown(mod);
        r.keyDown(KeyEvent.VK_V);
        r.keyUp(KeyEvent.VK_V);
        r.keyUp(mod);
        return this;
    }

    /**
     * copy the current selected item or text to the clipboard. Will do the same as "CTRL + V".
     *
     * @return this {@link Environment}.
     */
    @ModifySahiTimer
    @LogToResult(message = "copy the current selection to the clipboard", logClassInstance = false)
    public Environment copyIntoClipboard() {
        int mod = Key.getHotkeyModifier();
        IRobot r = loader.getScreen().getRobot();
        r.keyDown(mod);
        r.keyDown(KeyEvent.VK_C);
        r.keyUp(KeyEvent.VK_C);
        r.keyUp(mod);
        return this;
    }

    /**
     * {@link org.sakuli.actions.screenbased.TypingUtil#paste(String)}.
     */
    @ModifySahiTimer
    @LogToResult(logClassInstance = false)
    public Environment paste(String text) {
        return typingUtil.paste(text);
    }

    /**
     * {@link org.sakuli.actions.screenbased.TypingUtil#pasteMasked(String)}.
     */
    @ModifySahiTimer
    @LogToResult(logClassInstance = false, logArgs = false)
    public Environment pasteMasked(String text) {
        return typingUtil.pasteMasked(text);
    }

    /**
     * {@link org.sakuli.actions.screenbased.TypingUtil#pasteAndDecrypt(String)}.
     */
    @ModifySahiTimer
    @LogToResult(logClassInstance = false, logArgs = false)
    public Environment pasteAndDecrypt(String text) {
        return typingUtil.pasteAndDecrypt(text);
    }

    /**
     * See {@link org.sakuli.actions.screenbased.TypingUtil#type(String, String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "type over system keyboard", logClassInstance = false)
    public Environment type(String text) {
        return typingUtil.type(text, null);
    }

    /**
     * See {@link org.sakuli.actions.screenbased.TypingUtil#type(String, String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "type with pressed modifiers", logClassInstance = false)
    public Environment type(String text, String optModifiers) {
        return typingUtil.type(text, optModifiers);
    }

    /**
     * See {@link org.sakuli.actions.screenbased.TypingUtil#typeMasked(String, String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "type over system keyboard", logClassInstance = false, logArgs = false)
    public Environment typeMasked(String text) {
        return typingUtil.typeMasked(text, null);
    }

    /**
     * See {@link org.sakuli.actions.screenbased.TypingUtil#typeMasked(String, String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "type with pressed modifiers", logClassInstance = false, logArgs = false)
    public Environment typeMasked(String text, String optModifiers) {
        return typingUtil.typeMasked(text, optModifiers);
    }

    /**
     * See {@link org.sakuli.actions.screenbased.TypingUtil#typeAndDecrypt(String, String)} .
     */
    @ModifySahiTimer
    @LogToResult(message = "decrypt and type over system keyboard", logClassInstance = false, logArgs = false)
    public Environment typeAndDecrypt(String text) {
        return typingUtil.typeAndDecrypt(text, null);
    }

    /**
     * See {@link org.sakuli.actions.screenbased.TypingUtil#typeAndDecrypt(String, String)} .
     */
    @ModifySahiTimer
    @LogToResult(message = "decrypt and type with pressed modifiers", logClassInstance = false, logArgs = false)
    public Environment typeAndDecrypt(String text, String optModifiers) {
        return typingUtil.typeAndDecrypt(text, optModifiers);
    }

    /**
     * See {@link TypingUtil#keyDown(String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "press key down", logClassInstance = false)
    public Environment keyDown(String keys) {
        return typingUtil.keyDown(keys);
    }

    /**
     * See {@link TypingUtil#keyUp(String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "press key up", logClassInstance = false)
    public Environment keyUp(String keys) {
        return typingUtil.keyUp(keys);
    }

    /**
     * See {@link TypingUtil#write(String)}.
     */
    @ModifySahiTimer
    @LogToResult(message = "interpret and write the following expresion", logClassInstance = false)
    public Environment write(String text) {
        return typingUtil.write(text);
    }

    /**
     * {@link TypingUtil#mouseWheelDown(int)}
     */
    @LogToResult(message = "move mouse wheel down x times")
    public Environment mouseWheelDown(int steps) {
        return typingUtil.mouseWheelDown(steps);
    }

    /**
     * {@link TypingUtil#mouseWheelUp(int)}
     */
    @LogToResult(message = "move mouse wheel up x times")
    public Environment mouseWheelUp(int steps) {
        return typingUtil.mouseWheelUp(steps);
    }

    /**
     * {@link org.sakuli.actions.screenbased.TypingUtil#decryptSecret(String)}
     */
    @LogToResult(logClassInstance = false, logArgs = false)
    public String decryptSecret(String secret) {
        return typingUtil.decryptSecret(secret);
    }

    /**
     * @return true, if the OS is any instance of an Windows based OS
     */
    @LogToResult(logClassInstance = false, logArgs = false)
    public boolean isWindows() {
        switch (OSUtils.identifyOS()) {
            case "xp":
                return true;
            case "nt":
                return true;
            default:
                return false;
        }
    }

    /**
     * @return true, if the OS is any instance of an Linux based OS
     */
    @LogToResult(logClassInstance = false, logArgs = false)
    public boolean isLinux() {
        switch (OSUtils.identifyOS()) {
            case "linux":
                return true;
            default:
                return false;
        }
    }

    /**
     * @return a identifier of the current OS
     */
    @LogToResult(logClassInstance = false, logArgs = false)
    public String getOsIdentifier() {
        return OSUtils.identifyOS();
    }

    /**
     * Reads out the environment variable with the assigned key
     *
     * @return {@link String} value or null
     */
    @LogToResult(logClassInstance = false)
    public String getEnv(String key) {
        String result = System.getenv(key);
        return isBlank(result) ? null : result;
    }

    /**
     * Reads out the property value with the assigned key
     *
     * @return {@link String} value or null
     */
    @LogToResult(logClassInstance = false)
    public String getProperty(String key) {
        final Properties propsRef = BeanLoader.loadBean(SakuliPropertyPlaceholderConfigurer.class).getPropsRef();
        String result = (propsRef != null) ? propsRef.getProperty(key) : null;
        return isBlank(result) ? null : result;
    }

    @Override
    public boolean getResumeOnException() {
        return resumeOnException;
    }

    @Override
    public ScreenActionLoader getLoader() {
        return loader;
    }

    @Override
    public RegionImpl getActionRegion() {
        return RegionImpl.toRegion(loader.getScreen(), resumeOnException, loader);
    }
}
