from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from itertools import groupby
from operator import itemgetter
from math import sqrt, exp, pi, asin, acos, atan

import numpy as np
from scipy.integrate import quad
from scipy.stats import chi2
from scipy.special import expit, logit, xlogy
from mipframework import Algorithm, AlgorithmResult
from mipframework.highcharts import CalibrationBeltPlot
from mipframework import create_runner
from mipframework.constants import (
    PREC,
    MAX_ITER,
)
from utils.algorithm_utils import ExaremeError

class CalibrationBelt(Algorithm):
    def __init__(self, cli_args):
        super(CalibrationBelt, self).__init__(__file__, cli_args, intercept=False)

    def local_init(self):
        observed = np.array(self.data.variables.iloc[:, 1]).flatten()
        expected = np.array(self.data.covariables).flatten()
        max_deg = int(self.parameters.max_deg)
        if self.parameters.devel == "external":
            if not 2 <= max_deg <= 4:
                raise ExaremeError(
                    "Max deg should be between 2 and 4 for `devel`=`external`."
                )
        elif self.parameters.devel == "internal":
            if not 3 <= max_deg <= 4:
                raise ExaremeError(
                    "Max deg should be between 3 and 4 for `devel`=`internal`."
                )
        n_obs = len(expected)
        ge = logit(expected)

        Y = observed
        X = np.array([np.power(ge, i) for i in range(max_deg + 1)]).T
        Xs = np.broadcast_to(X, (4, n_obs, max_deg + 1))
        masks = np.zeros(Xs.shape, dtype=bool)
        for idx in range(0, max_deg - 1):
            masks[idx, :, idx + 2 :] = True
        Xs = np.ma.masked_array(Xs, mask=masks)

        n_y_pos = len(Y[Y == 1])
        n_y_neg = len(Y[Y == 0])

        self.store(Xs=Xs)
        self.store(Y=Y)
        self.push_and_add(n_obs=n_obs)
        self.push_and_add(n_y_pos=n_y_pos)
        self.push_and_add(n_y_neg=n_y_neg)

    def global_init(self):
        n_obs = self.fetch("n_obs")
        max_deg = int(self.parameters.max_deg)

        # if self.fetch("n_y_pos") < max_deg + 1 or self.fetch("n_y_neg") < max_deg + 1:
        #     raise ExaremeError("There are not enough data to perform this experiment.")

        iter_ = 0

        coeffs, lls = init_model(max_deg, n_obs)

        self.store(n_obs=n_obs)
        self.store(lls=lls)
        self.store(coeffs=coeffs)
        self.store(iter_=iter_)
        self.push(coeffs=coeffs)

    def local_step(self):
        Xs = self.load("Xs")
        Y = self.load("Y")
        max_deg = int(self.parameters.max_deg)
        coeffs = self.fetch("coeffs")

        grads, hessians, lls = update_local_model_parameters(Xs, Y, coeffs, max_deg)

        self.push_and_add(lls=lls)
        self.push_and_add(grads=grads)
        self.push_and_add(hessians=hessians)

    def global_step(self):
        max_deg = int(self.parameters.max_deg)
        lls_old = self.load("lls")
        iter_ = self.load("iter_")
        grads = self.fetch("grads")
        lls = self.fetch("lls")
        hessians = self.fetch("hessians")

        coeffs = update_coefficients(grads, hessians, max_deg)

        # Update termination quantities
        deltas = abs(lls - lls_old)
        if all(deltas < PREC) or iter_ >= MAX_ITER:
            self.terminate()
        iter_ += 1

        self.store(lls=lls)
        self.store(coeffs=coeffs)
        self.store(grads=grads)
        self.store(hessians=hessians)
        self.store(iter_=iter_)
        self.push(coeffs=coeffs)

    def local_final(self):
        Xs = self.load("Xs")
        Y = self.load("Y")

        log_like_bisector = compute_log_like_on_bisector(Xs, Y)

        e_min = min(np.array(self.data.covariables).flatten())
        e_max = max(np.array(self.data.covariables).flatten())

        self.push_and_agree(e_name=self.parameters.x[0])
        self.push_and_agree(o_name=self.parameters.y[0])
        self.push_and_add(log_like_bisector=log_like_bisector)
        self.push_and_min(e_min=e_min)
        self.push_and_max(e_max=e_max)

    def global_final(self):
        devel = self.parameters.devel
        threshold = float(self.parameters.thres)
        max_deg = int(self.parameters.max_deg)
        num_points = int(self.parameters.num_points)
        lls = self.load("lls")
        hessians = self.load("hessians")
        coeffs = self.load("coeffs")
        log_like_bisector = self.fetch("log_like_bisector")
        e_bounds = self.fetch("e_min"), self.fetch("e_max")
        confidence_levels = [float(cl) for cl in self.parameters.confLevels.split(",")]

        coeff, covariance, hess, ll, model_deg = select_model(
            coeffs, devel, hessians, lls, max_deg, threshold
        )

        p_value = compute_pvalue(devel, ll, log_like_bisector, model_deg, threshold)

        G, calibration_curve, e = compute_calibration_curve(coeff, num_points, e_bounds)

        calibration_belts, p_min, p_max = compute_calibration_belt(
            G, coeff, covariance, confidence_levels
        )

        over_bisect1 = find_relative_to_bisector(np.around(e, 4), p_min[0], "over")
        under_bisect1 = find_relative_to_bisector(np.around(e, 4), p_max[0], "under")
        over_bisect2 = find_relative_to_bisector(np.around(e, 4), p_min[1], "over")
        under_bisect2 = find_relative_to_bisector(np.around(e, 4), p_max[1], "under")

        cl1, cl2 = [float(cl) for cl in self.parameters.confLevels.split(",")]
        raw_data = {
            "Model degree": int(model_deg),
            "coeff": coeff.tolist(),
            "log-likelihood": ll,
            "n_obs": int(self.load("n_obs")),
            "seqP": np.around(e, 8).tolist(),
            "Calibration curve": np.around(calibration_curve, 4).tolist(),
            "Calibration belt 1": np.around(calibration_belts[0], 8).tolist(),
            "Calibration belt 2": np.around(calibration_belts[1], 8).tolist(),
            "p value": p_value,
            "Over bisector 1": over_bisect1,
            "Under bisector 1": under_bisect1,
            "Over bisector 2": over_bisect2,
            "Under bisector 2": under_bisect2,
            "Confidence level 1": str(int(cl1 * 100)) + "%",
            "Confidence level 2": str(int(cl2 * 100)) + "%",
            "Threshold": str(int(threshold * 100)) + "%",
            "Expected name": self.fetch("e_name"),
            "Observed name": self.fetch("o_name"),
        }

        calibration_belt1_hc = np.array([e, p_min[0], p_max[0]]).transpose()
        calibration_belt2_hc = np.array([e, p_min[1], p_max[1]]).transpose()
        chart = CalibrationBeltPlot(
            title="Calibration Belt",
            data=[calibration_belt1_hc.tolist(), calibration_belt2_hc.tolist()],
            confidence_levels=[cl1, cl2],
            e_name=self.fetch("e_name"),
            o_name=self.fetch("o_name"),
            model_deg=model_deg,
            thres=threshold,
            p_val=p_value,
            n=self.load("n_obs"),
            cl1=cl1,
            under1=under_bisect1,
            over1=over_bisect1,
            cl2=cl2,
            under2=under_bisect2,
            over2=over_bisect2,
        )

        self.result = AlgorithmResult(raw_data, highcharts=[chart])

def init_model(max_deg, n_obs):
    lls = np.ones((max_deg,), dtype=np.float) * (-2 * n_obs * np.log(2))
    coeffs = np.zeros((max_deg, max_deg + 1))
    masks = np.zeros(coeffs.shape, dtype=bool)
    for idx in range(0, max_deg - 1):
        masks[idx, idx + 2 :] = True
    coeffs = np.ma.masked_array(coeffs, mask=masks)
    return coeffs, lls

def update_local_model_parameters(Xs, Y, coeffs, max_deg):
    hessians = np.ma.empty((max_deg, coeffs.shape[1], coeffs.shape[1]), dtype=np.float)
    grads = np.ma.empty((max_deg, coeffs.shape[1]), dtype=np.float)
    lls = np.empty((max_deg,), dtype=np.float)
    for idx in range(max_deg):
        X = Xs[idx]
        coeff = coeffs[idx]

        z = np.ma.dot(X, coeff)
        s = expit(z)
        d = np.multiply(s, (1 - s))
        D = np.diag(d)

        hess = np.ma.dot(np.transpose(X), np.ma.dot(D, X))
        hessians[idx] = hess

        Ymsd = (Y - s) / d  # Stable computation of (Y - s) / d
        Ymsd[(Y == 0) & (s == 0)] = -1
        Ymsd[(Y == 1) & (s == 1)] = 1
        Ymsd = Ymsd.clip(-1e6, 1e6)
        grad = np.ma.dot(np.transpose(X), np.ma.dot(D, z + Ymsd))
        grads[idx] = grad

        ll = np.sum(xlogy(Y, s) + xlogy(1 - Y, 1 - s))
        lls[idx] = ll
    return grads, hessians, lls

def update_coefficients(grads, hessians, max_deg):
    coeffs = np.zeros((max_deg, max_deg + 1))
    masks = np.zeros(coeffs.shape, dtype=bool)
    for idx in range(0, max_deg - 1):
        masks[idx, idx + 2 :] = True
    coeffs = np.ma.masked_array(coeffs, mask=masks)
    if np.isinf(hessians).any():
        hessians = hessians.clip(-1e6, 1e6)
    if np.isinf(grads).any():
        grads = grads.clip(-1e6, 1e6)
    for idx in range(max_deg):
        hess = hessians[idx]
        covariance = np.ma.zeros(hess.shape)
        covariance.mask = hess.mask
        try:
            covariance[: idx + 2, : idx + 2] = np.linalg.inv(hess[: idx + 2, : idx + 2])
            if np.isnan(covariance[: idx + 2, : idx + 2]).any():
                raise np.linalg.LinAlgError
        except np.linalg.LinAlgError:
            covariance[: idx + 2, : idx + 2] = np.linalg.pinv(
                hess[: idx + 2, : idx + 2]
            )
        if (
            np.isclose(grads[idx], 0.0).any()
            and np.isinf(covariance[: idx + 2, : idx + 2]).any()
        ):
            covariance[: idx + 2, : idx + 2] = covariance[: idx + 2, : idx + 2].clip(
                -1e6, 1e6
            )

        coeffs[idx] = np.ma.dot(covariance, grads[idx])

    return coeffs

def compute_log_like_on_bisector(Xs, Y):
    X = Xs[0]
    coeff = np.zeros(X.shape[-1])
    coeff[1] = 1
    z = np.dot(X, coeff)
    s = expit(z)
    log_like_bisector = np.sum(xlogy(Y, s) + xlogy(1 - Y, 1 - s))
    return log_like_bisector

def select_model(coeffs, devel, hessians, lls, max_deg, thres):
    if devel == "external":
        idx = 0
    elif devel == "internal":
        idx = 1
    else:
        raise ValueError("devel should be `internal` or `external`")
    crit = chi2.ppf(q=thres, df=1)
    for i in range(idx + 1, max_deg):
        ddev = 2 * (lls[i] - lls[i - 1])
        if ddev > crit:
            idx = i
        else:
            break
    model_deg = idx + 1
    hess = hessians[idx]
    ll = lls[idx]
    coeff = coeffs[idx]
    coeff = coeff[~coeff.mask]
    covariance = np.linalg.inv(hess[: idx + 2, : idx + 2])
    return coeff, covariance, hess, ll, model_deg

def compute_pvalue(devel, ll, log_lik_bisector, model_deg, thres):
    calibration_stat = 2 * (ll - log_lik_bisector)
    p_value = 1 - giviti_stat_cdf(
        calibration_stat, m=model_deg, devel=devel, thres=thres
    )
    return p_value

def compute_calibration_curve(coeff, num_points, e_bounds):
    e_min, e_max = e_bounds
    e_lin = np.linspace(e_min, e_max, num=(int(num_points) + 1) // 2)
    e_log = expit(np.linspace(logit(e_min), logit(e_max), num=int(num_points) // 2))
    e = np.concatenate((e_lin, e_log))
    e = np.sort(e)
    ge = logit(e)
    G = np.array([np.power(ge, i) for i in range(len(coeff))]).T
    p = expit(np.dot(G, coeff))
    calibration_curve = np.array([e, p]).transpose()
    return G, calibration_curve, e

def compute_calibration_belt(G, coeff, covariance, confidence_levels):
    GVG = np.einsum("li, ij, lj -> l", G, covariance, G)
    sqrt_chi_GVG = np.sqrt(
        np.multiply(chi2.ppf(q=confidence_levels, df=2)[:, np.newaxis], GVG)
    )
    g_min = np.dot(G, coeff) - sqrt_chi_GVG
    g_max = np.dot(G, coeff) + sqrt_chi_GVG
    p_min, p_max = expit(g_min), expit(g_max)
    calibration_belts = np.array([p_min, p_max])
    calibration_belts = np.einsum("ijk -> jik", calibration_belts)
    return calibration_belts, p_min, p_max

def giviti_stat_cdf(t, m, devel="external", thres=0.95):
    assert m in {1, 2, 3, 4}, "m must be an integer from 1 to 4"
    assert 0 <= thres <= 1, "thres must be a number in [0, 1]"
    p_deg_inc = 1 - thres
    k = chi2.ppf(q=1 - p_deg_inc, df=1)
    cdf_value = None
    if devel == "external":
        if t <= (m - 1) * k:
            cdf_value = 0
        else:
            if m == 1:
                cdf_value = chi2.cdf(t, df=2)
            elif m == 2:
                cdf_value = (
                    chi2.cdf(t, df=1)
                    - 1
                    + p_deg_inc
                    + (-1) * sqrt(2) / sqrt(pi) * exp(-t / 2) * (sqrt(t) - sqrt(k))
                ) / p_deg_inc
            elif m == 3:
                integral1 = quad(
                    lambda y: (chi2.cdf(t - y, df=1) - 1 + p_deg_inc)
                    * chi2.pdf(y, df=1),
                    k,
                    t - k,
                )[0]
                integral2 = quad(
                    lambda y: (sqrt(t - y) - sqrt(k)) * 1 / sqrt(y), k, t - k
                )[0]
                num = integral1 - exp(-t / 2) / (2 * pi) * 2 * integral2
                den = p_deg_inc ** 2
                cdf_value = num / den
            elif m == 4:
                integral = quad(
                    lambda r: r ** 2
                    * (exp(-(r ** 2) / 2) - exp(-t / 2))
                    * (
                        -pi * sqrt(k) / (2 * r)
                        + 2 * sqrt(k) / r * asin((r ** 2 / k - 1) ** (-1 / 2))
                        - 2 * atan((1 - 2 * k / r ** 2) ** (-1 / 2))
                        + 2 * sqrt(k) / r * atan((r ** 2 / k - 2) ** (-1 / 2))
                        + 2 * atan(r / sqrt(k) * sqrt(r ** 2 / k - 2))
                        - 2 * sqrt(k) / r * atan(sqrt(r ** 2 / k - 2))
                    ),
                    sqrt(3 * k),
                    sqrt(t),
                )[0]
                cdf_value = (2 / (pi * p_deg_inc ** 2)) ** (3 / 2) * integral
    elif devel == "internal":
        assert m != 1, "if devel=`internal`, m must be an integer from 2 to 4"
        if t <= (m - 2) * k:
            cdf_value = 0
        else:
            if m == 2:
                cdf_value = chi2.cdf(t, df=1)
            elif m == 3:
                integral = quad(
                    lambda r: r * exp(-(r ** 2) / 2) * acos(sqrt(k) / r),
                    sqrt(k),
                    sqrt(t),
                )[0]
                cdf_value = 2 / (pi * p_deg_inc) * integral
            elif m == 4:
                integral = quad(
                    lambda r: r ** 2
                    * exp(-(r ** 2) / 2)
                    * (
                        atan(sqrt(r ** 2 / k * (r ** 2 / k - 2)))
                        - sqrt(k) / r * atan(sqrt(r ** 2 / k - 2))
                        - sqrt(k) / r * acos((r ** 2 / k - 1) ** (-1 / 2))
                    ),
                    sqrt(2 * k),
                    sqrt(t),
                )[0]
                cdf_value = (2 / pi) ** (3 / 2) * (p_deg_inc) ** (-2) * integral
    else:
        raise ValueError("devel argument must be either `internal` or `external`")
    if cdf_value < -0.001 or cdf_value > 1.001:
        raise ValueError("cdf_value outside [0,1].")
    elif -0.001 <= cdf_value < 0:
        return 0
    elif 1 < cdf_value <= 1.001:
        return 1
    else:
        return cdf_value

def find_relative_to_bisector(x, y, region):
    assert len(x) == len(y), "x and y must have the same length"
    if region == "over":
        reg = [yi > xi for yi, xi in zip(y, x)]
    elif region == "under":
        reg = [yi < xi for yi, xi in zip(y, x)]
    else:
        raise ValueError("region_type must either be `over` or `under`")
    idxreg = [i for i, o in enumerate(reg) if o]
    if len(idxreg) == 0:
        return "NEVER"
    segments = ""
    for k, g in groupby(enumerate(idxreg), lambda ix: ix[0] - ix[1]):
        seg = list(map(itemgetter(1), g))
        if x[seg[0]] != x[seg[-1]]:
            segments += str(x[seg[0]]) + "-" + str(x[seg[-1]]) + ", "
        else:
            segments += str(x[seg[0]]) + ", "
    segments = segments[:-2]
    return segments

if __name__ == "__main__":
    import time

    algorithm_args = [
        "-x",
        "probGiViTI_2017_Complessiva",
        "-y",
        "hospOutcomeLatest_RIC10",
        "-devel",
        "internal",
        "-max_deg",
        "4",
        "-confLevels",
        "0.80, 0.95",
        "-thres",
        "0.95",
        "-num_points",
        "200",
        "-pathology",
        "dementia",
        "-dataset",
        "cb_data",
        "-filter",
        "",
        "-formula",
        "",
    ]
    runner = create_runner(
        algorithm_class=CalibrationBelt, num_workers=1, algorithm_args=algorithm_args,
    )
    start = time.time()
    runner.run()
    end = time.time()
    print("Completed in ", end - start)


import json

class Module:

    def __init__(self, module):
        self.module = module
        self.max_num_q = len(self.module)
        self.min_num_q = len([m for m in self.module if "rules" not in m ])

    def get_current_page(self, question_id="1"):
        for page in self.module:
            if page["QID"] == question_id:
                return page
        raise ValueError("current page id: " + question_id + " cannot be found!")

    def next_page(self, question_id, answer_id_list, past_qna):
        next_page_id = None
        current_page = None
        current_answer = None

        for page in self.module:
            if page["QID"] == question_id:

                current_page = page
                for answer in page["answers"]:
                    # if multiple answer, start with the first one
                    if answer["AID"] == answer_id_list[0]:
                        current_answer = answer
                        next_page_id = answer["nextQID"]
                        break

                if current_answer is None:
                    raise ValueError("Current answer id: " + answer_id_list[0] + " cannot be found!")

        if current_page is None:
            raise ValueError("Current page id: " + question_id + " cannot be found!")

        if next_page_id:
            for page in self.module:
                if page["QID"] == next_page_id:
                    # here need to check if the next page has skip rules recursively
                    return self._skip_page(page, past_qna)
            raise ValueError("Next page id: " + next_page_id + " cannot be found!")
        else:
            # reach the end
            return None

    def _skip_page(self, page, past_qna):
        if "rules" in page.keys():
            past_answers_list = self._flatten_answers(past_qna)
            if page["rules"].get("operator") == "AND":
                match = True
                for criterion in page["rules"]["criteria"]:
                    if criterion["AID"] not in past_answers_list:
                        match = False
                if not match:
                    # skip this question and go to the next
                    return self.next_page(question_id=page["QID"],
                                          answer_id_list=[page["answers"][-1]["AID"]],
                                          past_qna=past_qna)
                else:
                    return page
        return page

    def prev_page(self, prev_question_id):
        if prev_question_id:
            for page in self.module:
                if page["QID"] == prev_question_id:
                    return page
            raise ValueError("Previous page id: " + prev_question_id + " cannot be found!")
        else:
            # reach the beginning
            return None

    def generate_qna_report(self, past_qna):
        """
        given question and answer map get their details
        :param past_qna: [{ "QID": xxx, "AID":[xxx, xxx, ...]}, ...]
        :return:
        """
        response = []

        # reverse the order so correct report order can be generated
        past_qna.reverse()
        for qna in past_qna:

            found_page = False
            for page in self.module:

                if page["QID"] == qna["QID"]:
                    found_page = True

                    found_answer = False
                    answer_list = []
                    for answer in page["answers"]:
                        if answer["AID"] in qna["AID"]:
                            found_answer = True
                            answer_list.append({
                                "AID": answer["AID"],
                                "prettyAID": answer.get("prettyAID"),
                                "answer": answer["answer"],
                                "description": answer["description"],
                                "resources": answer["resources"]
                            })

                    response.append({
                        "QID": page["QID"],
                        "question": page["question"],
                        "description": page["description"],
                        "resources": page["resources"],
                        "answers": answer_list
                    })

                    if not found_answer:
                        raise ValueError("AID: " + qna["AID"] + "not found!")

            if not found_page:
                raise ValueError("QID: " + qna["QID"] + " not found!")

        return response

    def compile_checklist(self, past_qna, reference: dict):
        """
        given question and answer map get their checklist
        can be connected with generate_qna_report
        :param past_qna:
        :param reference: [{"activity":..}]
        :return:
        """
        answer_id_list = self._flatten_answers(past_qna)
        checklist = []
        for reference_item in reference:
            operator = reference_item["rules"]["operator"]

            if operator == "AND":
                match = True
                for criterion in reference_item["rules"]["criteria"]:
                    if criterion["AID"] not in answer_id_list:
                        match = False
                if match:
                    checklist.append(reference_item)

            elif operator == "OR":
                match = False
                for criterion in reference_item["rules"]["criteria"]:
                    if criterion["AID"] in answer_id_list:
                        match = True
                if match:
                    checklist.append(reference_item)

            elif operator == "NOT":
                raise ValueError("Not rules not implemented yet!")

            elif operator == "ALL":
                checklist.append(reference_item)

            else:
                raise ValueError(operator + "rules not implemented yet!")

        return checklist

    @staticmethod
    def _flatten_answers(past_qna):
        answer_id_list = []
        for qna in past_qna:
            for id in qna["AID"]:
                answer_id_list.append(id)

        return answer_id_list

if __name__ == "__main__":
    distancing_decision_module = Module("decisiontrees/distancing_decision.json")
    with open("decisiontrees/distancing_checklist.json", "r") as f:
        ref = json.load(f)
    past_qna = [{"QID": "4b", "AID": ["4b-iv"]}, {"QID": "4", "AID": ["4b", "4c"]}, {"QID": "3c", "AID": ["3c-i"]},
                {"QID": "3b", "AID": ["3b-iv-1"]}, {"QID": "3", "AID": ["3b", "3c"]},
                {"QID": "2b-ii", "AID": ["2b-ii-1"]}, {"QID": "2b-i", "AID": ["2b-i-3"]}, {"QID": "2b", "AID": ["2b"]},
                {"QID": "2a-ii-3", "AID": ["2a-ii-3-b"]}, {"QID": "2a-ii-2", "AID": ["2a-ii-2"]},
                {"QID": "2a-ii-1", "AID": ["2a-ii-1-a"]}, {"QID": "2a", "AID": ["2a-ii"]}, {"QID": "2", "AID": ["2"]},
                {"QID": "1c", "AID": ["1c-yes"]}, {"QID": "1b", "AID": ["1b-i"]}, {"QID": "1a", "AID": ["1a-i"]},
                {"QID": "1", "AID": ["1"]}]
    # print(distancing_decision_module.next_page(question_id="3", answer_id_list=["3a", "3c"], past_qna=past_qna))
    report = distancing_decision_module.generate_qna_report(past_qna)
    print(report)
    # checklist = distancing_decision_module.compile_checklist(past_qna, ref)
    # print(checklist)


# Project Modules
from utils import *

# Python Modules
from os import path
import threading, sys, traceback, logging

# Qt Modules
from PySide2.QtCore import *
from PySide2.QtWidgets import *
from PySide2.QtGui import *
import shiboken2 as shiboken

# Maya Modules
from maya import cmds, mel
from maya.api import OpenMaya, OpenMayaAnim, OpenMayaUI
import maya.utils as mayaUtils

# ======================================================================================================================
#
#
# Global
#
#
# ======================================================================================================================

__version__ = '1.0.4'
__author__ = 'Justin Tirado'

UI_NAME = 'stop_motion_UI'
UI_TITLE = 'Stop Motion Tool - {}'.format(__version__)

DIRECTORY = path.dirname(path.abspath(__file__))
IMAGE_HEADER = path.join(DIRECTORY, 'image_header.png')

# ======================================================================================================================
#
#
# Function
#
#
# ======================================================================================================================

def maya_to_qt(name, *args, **kwargs):
	"""
	Maya to QWidget

	:param str name:    Maya name of an ui object
	:return:            QWidget of parsed Maya name
	:rtype:             QWidget
	"""
	ptr = OpenMayaUI.MQtUtil.findControl(name)
	if ptr is None:
		ptr = OpenMayaUI.MQtUtil.findLayout(name)
	if ptr is None:
		ptr = OpenMayaUI.MQtUtil.findMenuItem(name)
	if ptr is not None:
		return shiboken.wrapInstance(long(ptr), QWidget)
	return ptr

def qt_to_maya(widget, *args, **kwargs):
	"""
	QWidget to Maya name

	:param QWidget widget:  QWidget of a maya ui object
	:return:                Maya name of parsed QWidget
	:rtype:                 str
	"""
	return OpenMayaUI.MQtUtil.fullName(long(shiboken.getCppPointer(widget)[0]))

def get_maya_window(*args, **kwargs):
	ptr = OpenMayaUI.MQtUtil.mainWindow()
	if ptr is not None:
		return shiboken.wrapInstance(long(ptr), QWidget)

# ======================================================================================================================
#
#
# Class
#
#
# ======================================================================================================================

class WorkerSignals(QObject):
	finished = Signal()
	error = Signal(tuple)
	result = Signal(object)
	progress = Signal(int)
	running = Signal(bool)
	status = Signal(str)

class WorkerRunnable(QRunnable):
	"""
	Worker Thread
	"""

	def __init__(self, fn, *args, **kwargs):
		QRunnable.__init__(self)
		self.fn = fn
		self.args = args
		self.kwargs = kwargs
		self.signals = WorkerSignals()
		self.kwargs['progress_callback'] = self.signals.progress
		self.kwargs['progress_status'] = self.signals.status

	@Slot()
	def run(self):
		try:
			result = self.fn(*self.args, **self.kwargs)

		except:
			traceback.print_exc()
			exctype, value = sys.exc_info()[:2]
			self.signals.error.emit((exctype, value, traceback.format_exc()))
		else:
			self.signals.result.emit(result)

		finally:
			self.signals.finished.emit()
		return

class WorkerThread(QThread):
	"""
	Worker Thread
	"""

	def __init__(self, fn, *args, **kwargs):
		super(WorkerThread, self).__init__()
		self.fn = fn
		self.args = args
		self.kwargs = kwargs
		self.signals = WorkerSignals()
		self.kwargs['progress_callback'] = self.signals.progress
		self.kwargs['progress_status'] = self.signals.status

	@Slot()
	def run(self):
		try:
			result = self.fn(*self.args, **self.kwargs)

		except:
			traceback.print_exc()
			exctype, value = sys.exc_info()[:2]
			self.signals.error.emit((exctype, value, traceback.format_exc()))
		else:
			self.signals.result.emit(result)

		finally:
			self.signals.finished.emit()
		return

class FloatSlider(QWidget):
	valueChanged = Signal(float)

	def __init__(self, *args, **kwargs):
		QWidget.__init__(self, *args, **kwargs)

		self.currentValue = 0.5

		# Layout
		self.setLayout(QHBoxLayout())

		# Label
		self.label = QLabel('Favor Keys')
		self.layout().addWidget(self.label)

		# Float Field
		self.float_field = QDoubleSpinBox()
		self.float_field.setMinimum(1.0)
		self.float_field.setMaximum(2.0)
		self.float_field.setValue(1.5)
		self.float_field.setSingleStep(0.25)
		self.float_field.setFocusPolicy(Qt.ClickFocus)
		self.layout().addWidget(self.float_field)

		# Float Slider
		label_layout = QGridLayout()
		self.layout().addLayout(label_layout)

		self.float_slider = QSlider(Qt.Horizontal)
		self.float_slider.setMinimum(100)
		self.float_slider.setMaximum(200)
		self.float_slider.setValue(150)
		self.float_slider.setMinimumWidth(225)

		label_layout.addWidget(self.float_slider, 0, 0, 1, 2)

		label1 = QLabel("1's")
		label1.setAlignment(Qt.AlignLeft)
		label_layout.addWidget(label1, 1, 0, 1, 1)

		label2 = QLabel("2's")
		label2.setAlignment(Qt.AlignCenter)
		label_layout.addWidget(label2, 1, 2, 1, 1)

		# Slots
		self.float_field.valueChanged.connect(self.field_callback)
		self.float_slider.sliderReleased.connect(self.slider_callback)

	def slider_callback(self, *args, **kwargs):
		value = self.float_slider.value()
		self.float_field.setValue(float(value) / 100.0)
		self.currentValue = float(value - 100.0) / 100.0
		self.valueChanged.emit(float(value - 100.0) / 100.0)
		return

	def field_callback(self, *args, **kwargs):
		value = self.float_field.value()
		self.float_slider.setValue(int(value * 100.0))
		self.currentValue = float(value - 1.0)
		self.valueChanged.emit(float(value - 1.0))
		return

class Window(QMainWindow):
	status_init = 'init'
	status_abort = 'abort'
	status_error = 'error'
	status_warning = 'warning'
	status_busy = 'busy'
	status_finished = 'finished'
	status_preview = 'preview'

	state_init = 'Initializing'
	state_analyze = 'Analyzing'
	state_bake_dyn = 'Baking Dynamics'
	state_bake_layer = 'Baking to New Animation Layer'
	state_simulate = 'Removing Keys'
	state_cleanup = 'Cleanup'
	state_finished = 'Done'
	state_busy = 'Calculating ...'

	progress_callback = Signal(int)
	progress_status = Signal(str)

	def __init__(self, *args, **kwargs):
		QMainWindow.__init__(self, *args, **kwargs)

		# Variables
		self.status_bar_message = 'Ready'
		self.time_range = (1001, 1200)
		self.current_progress = 0
		self.min_progress = 0
		self.max_progress = 100

		self.animLayer = None
		self.animCurvesBuffer = None
		self.animCurvesProcessed = None
		self.preview_active = False
		self.start = None
		self.end = None
		self.originalCurves = None
		self.bufferCurvesState = True

		self.is_simulate = True
		self.is_layer = True
		self.is_dynamic = False
		self.ratio = 0.5
		self.selected = []

		# Layout
		self.widget_main = QWidget()
		self.widget_main.setLayout(QVBoxLayout())
		self.widget_main.layout().setContentsMargins(0, 0, 0, 0)
		self.setCentralWidget(self.widget_main)
		self.setMinimumWidth(400)
		self.setMaximumWidth(400)

		# Status
		self.status_bar = QStatusBar()
		self.status_bar.setSizeGripEnabled(False)
		self.status_bar.showMessage(self.status_bar_message)
		self.status_bar.setStyleSheet('background:black;')
		self.setStatusBar(self.status_bar)

		# Progress Bar
		self.progress_bar = QProgressBar()
		self.progress_bar.setRange(0, 100)
		self.progress_bar.setValue(0)
		self.progress_bar.setMaximumWidth(50)
		self.status_bar.addPermanentWidget(self.progress_bar)

		# Header
		self.header = QLabel()
		self.header.setPixmap(QPixmap(IMAGE_HEADER))
		self.widget_main.layout().addWidget(self.header)

		# Contents
		contents_layout = QVBoxLayout()
		contents_layout.setSpacing(10)
		contents_layout.setContentsMargins(10, 10, 10, 10)
		self.widget_main.layout().addLayout(contents_layout)

		# Step Mode
		self.group_step = QGroupBox('Auto Stepped Mode')
		self.group_step.setCheckable(True)
		self.group_step.setChecked(True)
		self.group_step.setFocusPolicy(Qt.NoFocus)
		self.group_step.setLayout(QVBoxLayout())
		self.group_step.setStatusTip('Favor keys between on 1s and on 2s.')
		self.group_step.installEventFilter(self)
		contents_layout.addWidget(self.group_step)

		# Ratio Slider
		self.ratio_slider = FloatSlider()
		self.group_step.layout().addWidget(self.ratio_slider)

		# Options
		self.group_options = QGroupBox('Options')
		self.group_options.setLayout(QVBoxLayout())
		contents_layout.addWidget(self.group_options)

		# Row Layout
		rowLayout = QHBoxLayout()
		self.group_options.layout().addLayout(rowLayout)

		# New Anim Layer
		self.checkbox_layer = QCheckBox('Bake To Animation Layer')
		self.checkbox_layer.setChecked(True)
		self.checkbox_layer.installEventFilter(self)
		self.checkbox_layer.setStatusTip('Bake Results to a new override animation layer.')
		rowLayout.addWidget(self.checkbox_layer)

		# Dynamics
		self.checkbox_dyn = QCheckBox('Bake Dynamics')
		self.checkbox_dyn.setChecked(False)
		self.checkbox_dyn.installEventFilter(self)
		self.checkbox_dyn.setStatusTip('Bake Dynamics on Apex Character Rigs Only (Watson, Mirage)')
		rowLayout.addWidget(self.checkbox_dyn)

		# Run Buttons
		run_button_layout = QHBoxLayout()
		run_button_layout.setContentsMargins(0, 0, 0, 0)
		contents_layout.addLayout(run_button_layout)

		# Preview Button
		self.button_preview = QPushButton('Preview (Experimental)')
		self.button_preview.setCheckable(True)
		self.button_preview.setStatusTip('Preview Stepped Mode. UNDO is disabled in preview.')
		self.button_preview.installEventFilter(self)
		run_button_layout.addWidget(self.button_preview)

		# Simulate Button
		self.button_simulate = QPushButton('Apply')
		self.button_simulate.setStatusTip('Apply simulation on selected objects.')
		self.button_simulate.installEventFilter(self)
		run_button_layout.addWidget(self.button_simulate)

		# Spacer
		spacer = QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
		self.widget_main.layout().addItem(spacer)

		# Thread
		# self.thread_pool = QThreadPool()
		# self.thread_count = self.thread_pool.maxThreadCount()
		# self.set_status("Max CPUs available\t:\t{}".format(self.thread_count))

		# Slots
		self.ratio_slider.valueChanged.connect(self.update_preview_worker)

		self.button_simulate.clicked.connect(self.apply_callback)
		self.button_preview.clicked.connect(self.preview_callback)

	# self.simulate_thread.progress_update.connect(self.run_widget.set_progress_current)
	# self.simulate_thread.state_changed.connect(self.update_status)
	# self.simulate_thread.progress_finished.connect(self.run_widget.finished_progress)
	# self.simulate_thread.progress_finished.connect(self.status_done)

	# ==================================================================================================================
	# Status
	# ==================================================================================================================

	def update_status(self, status, kind=None, *args, **kwargs):
		self.status_bar.showMessage(status)

		color = 'black'

		if kind in [self.status_warning, self.status_error]:
			color = 'darkred'

		elif kind == self.status_finished:
			color = 'green'

		self.status_bar.setStyleSheet('background:{};'.format(color))
		return

	def eventFilter(self, obj, event):
		if not self.preview_active:
			if event.type() == QEvent.Enter:
				self.status_bar.showMessage(obj.statusTip(), 0)
				self.status_bar.setStyleSheet('background:black;')
		event.accept()
		return False

	# ==================================================================================================================
	# Preview
	# ==================================================================================================================

	def get_options(self):
		self.is_simulate = self.group_step.isChecked()
		self.is_layer = self.checkbox_layer.isChecked()
		self.is_dynamic = self.checkbox_dyn.isChecked()
		self.ratio = self.ratio_slider.currentValue
		self.time_range = get_time_range()

		# Get Selected
		self.selected = cmds.ls(sl=True)

		if not self.selected:
			self.abort_preview()
			self.update_status('Nothing Selected.', self.status_warning)
			raise
		return

	def update_progress(self, value):
		if value > self.max_progress:
			self.progress_bar.setValue(self.max_progress)
		else:
			self.progress_bar.setValue(value)
		return

	def preview_callback(self):
		if self.button_preview.isChecked():
			if not self.group_step.isChecked():
				self.group_step.setChecked(True)

			self.start_preview()
		else:
			self.cancel_preview()
		return

	def abort_preview(self):
		self.button_preview.setChecked(False)
		return

	def cancel_preview(self):
		paste_clipboard_curves(self.originalCurves, self.start, self.end)
		self.animCurvesBuffer = None
		self.animCurvesProcessed = None
		self.preview_active = False
		cmds.undoInfo(stateWithoutFlush=True)
		return

	def start_preview(self):
		self.update_status('Calculating...', self.state_busy)
		self.get_options()

		# Original Curves
		self.originalCurves, self.start, self.end = copy_original_curves(self.selected)

		if self.originalCurves is None:
			self.abort_preview()
			self.update_status('No animation found.', self.status_error)
			return

		# Undo
		cmds.undoInfo(stateWithoutFlush=False)

		# Run
		self.preview_active = True
		self.progress_bar.setValue(0)
		self.run_buffer_worker()
		self.run_preview_worker(self.ratio_slider.currentValue)
		return

	def update_preview_worker(self, ratio):
		if self.preview_active:
			self.run_preview_worker(ratio)
		return

	def run_preview_worker(self, ratio):
		worker = WorkerThread(fn=self.refreshFilter,
		                      ratio=ratio
		                      )

		worker.signals.progress.connect(self.update_progress)
		worker.signals.status.connect(self.status_bar.showMessage)
		worker.run()
		return

	def apply_callback(self):
		self.get_options()

		if self.preview_active:
			# Get Original Curves Back
			cmds.undoInfo(openChunk=True)

			try:
				self.apply_curves(self.animCurvesBuffer,
				                  self.animCurvesBuffer,
				                  start=self.time_range[0],
				                  end=self.time_range[1],
				                  progress_callback=self.progress_callback,
				                  progress_status=self.progress_status,
				                  )
			finally:
				cmds.undoInfo(closeChunk=True)

			# apply processed curve for undo step
			cmds.undoInfo(swf=True)
			cmds.undoInfo(openChunk=True)

			try:
				self.apply_curves(self.animCurvesBuffer,
				                  self.animCurvesProcessed,
				                  start=self.time_range[0],
				                  end=self.time_range[1],
				                  progress_callback=self.progress_callback,
				                  progress_status=self.progress_status,
				                  )
			finally:
				cmds.undoInfo(closeChunk=True)

		else:
			self.run_apply_worker()

		self.button_preview.setChecked(False)
		self.progress_bar.setValue(0)
		self.animCurvesBuffer = None
		self.animCurvesProcessed = None
		self.preview_active = False
		return

	def run_apply_worker(self):
		worker = WorkerThread(fn=self.simulateFilter,
		                      )

		worker.signals.progress.connect(self.update_progress)
		worker.signals.status.connect(self.status_bar.showMessage)
		worker.run()
		return

	def refreshFilter(self, ratio, progress_callback, progress_status):
		self.animCurvesProcessed = self.generate_stop_motion_curves(self.animCurvesBuffer,
		                                                            ratio=ratio,
		                                                            start=self.time_range[0],
		                                                            end=self.time_range[1],
		                                                            progress_callback=progress_callback,
		                                                            progress_status=progress_status,
		                                                            )
		self.apply_curves(self.animCurvesBuffer,
		                  self.animCurvesProcessed,
		                  start=self.time_range[0],
		                  end=self.time_range[1],
		                  progress_callback=progress_callback,
		                  progress_status=progress_status,
		                  )
		progress_status.emit('Previewing Stop Motion Keys\t:\t{}'.format(ratio + 1.0))
		return

	def buffer_worker_result(self, result):
		self.animCurvesBuffer = result
		return

	def run_buffer_worker(self):
		worker = WorkerThread(fn=self.get_curves,
		                      nodes=cmds.ls(sl=True),
		                      start=self.time_range[0],
		                      end=self.time_range[1],
		                      )

		worker.signals.progress.connect(self.update_progress)
		worker.signals.status.connect(self.status_bar.showMessage)
		worker.signals.result.connect(self.buffer_worker_result)
		worker.run()
		return

	def get_curves(self, nodes, start, end, progress_callback, progress_status):
		# Signals
		progress_callback.emit(0)
		progress_status.emit('Getting Animation Data')
		step = 100.0 / float(len(nodes))
		current = 0.0

		result = {}
		curves = cmds.keyframe(nodes, q=True, name=True)

		for curve in curves:
			curve_dict = {}

			for frame in range(start, end + 1):
				curve_dict[frame] = cmds.keyframe(curve, q=True, time=(frame, frame), ev=True)[0]

			result[curve] = curve_dict

			current += step
			progress_callback.emit(current)

		progress_callback.emit(100)
		return result

	def curves_value_change(self, curves, start, end, progress_callback, progress_status):
		# Signals
		progress_status.emit('Calculating Changes in Animation')
		progress_callback.emit(0)
		step = 100.0 / float(len(curves))
		current = 0.0

		result = []

		# Get Max Values
		for frame in range(start, end):
			values = []

			for curve in curves:
				value2 = float(curves[curve][frame + 1])
				value1 = float(curves[curve][frame])

				value_change = (value2 - value1) / 100.0
				values.append(abs(value_change))

			result.append(max(values))
			current += step
			progress_callback.emit(current)

		progress_callback.emit(100)
		return result

	def generate_stop_motion_curves(self, curves, ratio, start, end, progress_callback, progress_status):
		# Get Counter
		counter = self.curves_value_change(curves, start, end, progress_callback, progress_status)
		counter_sorted = sorted(counter)

		# Threshold
		threshold = counter_sorted[0] + ((counter_sorted[-1] - counter_sorted[0]) * ratio)

		key = 0
		remove = 0
		step = 0

		remove_list = []

		# Loop
		for frame in range(start, end):
			if key == 1:
				key = 0
				remove_list.append(frame)
				remove = remove + 1

			elif key == 0:
				value_change = counter[frame - start]

				if value_change == 0:
					key = 1
					remove_list.append(frame)
					remove = remove + 1

				if value_change < threshold:
					key = 1
					step = step + 1

				elif value_change > threshold:
					key = 0
					step = step + 1

		# Signals
		progress_status.emit('Generating Stop Motion')
		progress_callback.emit(0)
		step = 100.0 / float(len(curves))
		current = 0.0

		result = {}

		for curve in curves:
			key_dict = {}

			for frame in curves[curve]:
				if frame not in remove_list:
					key_dict[frame] = curves[curve][frame]

			result[curve] = key_dict

			current += step
			progress_callback.emit(current)

		progress_callback.emit(100)
		return result

	def apply_curves(self, original, processed, start, end, progress_callback, progress_status):
		# Signals
		progress_status.emit('Removing Keys')
		progress_callback.emit(0)
		step = 100.0 / float(len(original))
		current = 0.0

		for curve in original:
			set_curve_to_step(curve)
			try:
				cmds.cutKey(curve,
				            time=(start + 0.001, end - 0.001),
				            option="keys",
				            cl=True
				            )
				self.add_keys(curve,
				              processed[curve],
				              )

			except RuntimeError:
				pass

			current += step
			progress_callback.emit(current)

		progress_callback.emit(100)
		return

	def add_keys(self, curve, anim_dict):
		"""
		Add keyframes to animation curve

		:param curve: animation curve name
		:param anim_dict: dictionary of keyframes in {frame_number (float): value (float)} format
		:return: None
		"""

		unit = OpenMaya.MTime.uiUnit()
		nodeNattr = cmds.listConnections(curve, d=True, s=False, p=True)[0]
		selList = OpenMaya.MSelectionList()
		selList.add(nodeNattr)
		mplug = selList.getPlug(0)
		dArrTimes = OpenMaya.MTimeArray()
		dArrVals = OpenMaya.MDoubleArray()

		if 'rotate' in nodeNattr:
			for i in anim_dict.keys():
				dArrTimes.append(OpenMaya.MTime(float(i), unit))
				dArrVals.append(OpenMaya.MAngle.uiToInternal(anim_dict[i]))
		else:
			for i in anim_dict.keys():
				dArrTimes.append(OpenMaya.MTime(float(i), unit))
				dArrVals.append(anim_dict[i])

		crvFnc = OpenMayaAnim.MFnAnimCurve(mplug)
		crvFnc.addKeys(dArrTimes, dArrVals, crvFnc.kTangentAuto, crvFnc.kTangentStep)
		return

	# ==================================================================================================================
	# Apply
	# ==================================================================================================================

	@undo
	def simulateFilter(self, progress_callback, progress_status):
		# Signals
		progress_status.emit('Simulating...')
		progress_callback.emit(0)
		step = 100.0 / float(len(self.selected))
		current = 0.0

		layer_name = None
		layer_exceptions = []
		rig = None
		rig_names = []
		new_items = []

		# Get items
		for item in self.selected:
			namespace = item.split(':')[0]

			if namespace:
				rig = Apex_Rig(namespace=namespace)

				if rig.main_controls:
					for control in rig.main_controls:
						if control not in self.selected:
							new_items.append(control)

				if rig.dyn_controls:
					layer_exceptions += rig.dyn_controls

				if hasattr(Apex_Name, namespace):
					rig_name = getattr(Apex_Name, namespace)

					if rig_name not in rig_names:
						rig_names.append(rig_name)

		if new_items:
			self.selected += new_items

		if rig_names:
			layer_name = 'stopMotion_layer1'
			for rig_name in rig_names:
				layer_name = '{}_{}'.format(rig_name, layer_name)

		# Dynamics
		if self.is_dynamic and rig:
			if rig.dyn_joints:
				rig.transfer_dyn_to_controls()

		# Animation Layers
		if self.is_layer:
			clean_anim_layers()

			if not layer_name:
				layer_name = 'stoppedMotion1'

			layer_name = create_anim_layer(layer_name)

			# Bake
			bake_to_layer(self.selected, layer_name)

		# Simulate
		if self.is_simulate:
			generate_stop_motion(self.selected,
			                     ratio=self.ratio,
			                     randomness=0.0,
			                     start=self.time_range[0],
			                     end=self.time_range[1],
			                     layer=layer_name,
			                     )

		# Exceptions
		if self.is_layer and layer_exceptions:
			for item in layer_exceptions:
				remove_from_anim_layer(item, layer_name)

		progress_callback.emit(0)
		progress_status.emit('Done')
		return

	# ==================================================================================================================
	# Close
	# ==================================================================================================================

	def closeEvent(self, *args, **kwargs):
		QMainWindow.closeEvent(self, *args, **kwargs)

		if self.preview_active:
			self.cancel_preview()
		self.deleteLater()
		return

# ======================================================================================================================
#
#
# Execute
#
#
# ======================================================================================================================

def show(name=UI_NAME, title=UI_TITLE, *args, **kwargs):
	if cmds.window(name, exists=True):
		cmds.deleteUI(name, wnd=True)

	# Window
	window = Window(get_maya_window())
	window.setObjectName(name)
	window.setWindowTitle(title)
	window.show()
	return

if __name__ == '__main__':
	pass


from ossie.cf import CF, CF__POA
from ossie.utils import uuid

from ossie.resource import Resource
from ossie.properties import simple_property

import Queue, copy, time, threading
from ossie.resource import usesport, providesport
from ossie.cf import ExtendedCF
from omniORB import CORBA
import struct #@UnresolvedImport
#from bulkio.bulkioInterfaces import BULKIO, BULKIO__POA #@UnusedImport
from ossie.events import PropertyEventSupplier
import bulkio

NOOP = -1
NORMAL = 0
FINISH = 1
class ProcessThread(threading.Thread):
    def __init__(self, target, pause=0.0125):
        threading.Thread.__init__(self)
        self.setDaemon(True)
        self.target = target
        self.pause = pause
        self.stop_signal = threading.Event()

    def stop(self):
        self.stop_signal.set()

    def updatePause(self, pause):
        self.pause = pause

    def run(self):
        state = NORMAL
        while (state != FINISH) and (not self.stop_signal.isSet()):
            state = self.target()
            if (state == NOOP):
                # If there was no data to process sleep to avoid spinning
                time.sleep(self.pause)

class Python_Ports_base(CF__POA.Resource, Resource):
        # These values can be altered in the __init__ of your derived class

        PAUSE = 0.0125 # The amount of time to sleep if process return NOOP
        TIMEOUT = 5.0 # The amount of time to wait for the process thread to die when stop() is called
        DEFAULT_QUEUE_SIZE = 100 # The number of BulkIO packets that can be in the queue before pushPacket will block

        def __init__(self, identifier, execparams):
            loggerName = (execparams['NAME_BINDING'].replace('/', '.')).rsplit("_", 1)[0]
            Resource.__init__(self, identifier, execparams, loggerName=loggerName)
            self.threadControlLock = threading.RLock()
            self.process_thread = None
            # self.auto_start is deprecated and is only kept for API compatability
            # with 1.7.X and 1.8.0 components.  This variable may be removed
            # in future releases
            self.auto_start = False

        def initialize(self):
            Resource.initialize(self)

            self.port_dataCharIn = bulkio.InCharPort( "dataCharIn")
            self.port_dataOctetIn = bulkio.InOctetPort( "dataOctetIn")
            self.port_dataShortIn = bulkio.InShortPort( "dataShortIn")
            self.port_dataUShortIn = bulkio.InUShortPort( "dataUShortIn")
            self.port_dataLongIn = bulkio.InLongPort( "dataLongIn")
            self.port_dataULongIn = bulkio.InULongPort( "dataULongIn")
            self.port_dataLongLongIn = bulkio.InLongLongPort( "dataLongLongIn")
            self.port_dataULongLongIn = bulkio.InULongLongPort( "dataULongLongIn")
            self.port_dataFloatIn = bulkio.InFloatPort( "dataFloatIn")
            self.port_dataDoubleIn = bulkio.InDoublePort( "dataDoubleIn")
            self.port_dataFileIn = bulkio.InFilePort( "dataFileIn")
            self.port_dataXMLIn = bulkio.InXMLPort( "dataXMLIn")
            self.port_dataSDDSIn = bulkio.InSDDSPort( "dataSDDSIn")

            self.port_dataCharOut = bulkio.OutCharPort( "dataCharOut")
            self.port_dataOctetOut = bulkio.OutOctetPort( "dataOctetOut")
            self.port_dataShortOut = bulkio.OutShortPort( "dataShortOut")
            self.port_dataUShortOut = bulkio.OutUShortPort( "dataUShortOut")
            self.port_dataLongOut = bulkio.OutLongPort( "dataLongOut")
            self.port_dataULongOut = bulkio.OutULongPort( "dataULongOut")
            self.port_dataLongLongOut = bulkio.OutLongLongPort( "dataLongLongOut")
            self.port_dataULongLongOut = bulkio.OutULongLongPort( "dataULongLongOut")
            self.port_dataFloatOut = bulkio.OutFloatPort( "dataFloatOut")
            self.port_dataDoubleOut = bulkio.OutDoublePort( "dataDoubleOut")
            self.port_dataFileOut = bulkio.OutFilePort( "dataFileOut")
            self.port_dataXMLOut = bulkio.OutXMLPort( "dataXMLOut")
            self.port_dataSDDSOut = bulkio.OutSDDSPort( "dataSDDSOut")

            self.port_propEvent = PropertyEventSupplier(self)

        def start(self):
            self.threadControlLock.acquire()
            try:
                Resource.start(self)
                if self.process_thread == None:
                    self.process_thread = ProcessThread(target=self.process, pause=self.PAUSE)
                    self.process_thread.start()
            finally:
                self.threadControlLock.release()

        def process(self):
            """The process method should process a single "chunk" of data and then return.  This method will be called
            from the processing thread again, and again, and again until it returns FINISH or stop() is called on the
            component.  If no work is performed, then return NOOP"""
            raise NotImplementedError

        def stop(self):
            self.threadControlLock.acquire()
            try:
                process_thread = self.process_thread
                self.process_thread = None

                if process_thread != None:
                    process_thread.stop()
                    process_thread.join(self.TIMEOUT)
                    if process_thread.isAlive():
                        raise CF.Resource.StopError(CF.CF_NOTSET, "Processing thread did not die")
                Resource.stop(self)
            finally:
                self.threadControlLock.release()

        def releaseObject(self):
            try:
                self.stop()
            except Exception:
                self._log.exception("Error stopping")
            self.threadControlLock.acquire()
            try:
                Resource.releaseObject(self)
            finally:
                self.threadControlLock.release()

        port_dataCharIn = providesport(name="dataCharIn",
                                            repid="IDL:BULKIO/dataChar:1.0",
                                            type_="control",)

        port_dataOctetIn = providesport(name="dataOctetIn",
                                            repid="IDL:BULKIO/dataOctet:1.0",
                                            type_="control",)

        port_dataShortIn = providesport(name="dataShortIn",
                                            repid="IDL:BULKIO/dataShort:1.0",
                                            type_="control",)

        port_dataUShortIn = providesport(name="dataUShortIn",
                                            repid="IDL:BULKIO/dataUshort:1.0",
                                            type_="control",)

        port_dataLongIn = providesport(name="dataLongIn",
                                            repid="IDL:BULKIO/dataLong:1.0",
                                            type_="control",)

        port_dataULongIn = providesport(name="dataULongIn",
                                            repid="IDL:BULKIO/dataUlong:1.0",
                                            type_="control",)

        port_dataLongLongIn = providesport(name="dataLongLongIn",
                                            repid="IDL:BULKIO/dataLongLong:1.0",
                                            type_="control",)

        port_dataULongLongIn = providesport(name="dataULongLongIn",
                                            repid="IDL:BULKIO/dataUlongLong:1.0",
                                            type_="control",)

        port_dataFloatIn = providesport(name="dataFloatIn",
                                            repid="IDL:BULKIO/dataFloat:1.0",
                                            type_="control",)

        port_dataDoubleIn = providesport(name="dataDoubleIn",
                                            repid="IDL:BULKIO/dataDouble:1.0",
                                            type_="control",)

        port_dataFileIn = providesport(name="dataFileIn",
                                            repid="IDL:BULKIO/dataFile:1.0",
                                            type_="control",)

        port_dataXMLIn = providesport(name="dataXMLIn",
                                            repid="IDL:BULKIO/dataXML:1.0",
                                            type_="control",)

        port_dataSDDSIn = providesport(name="dataSDDSIn",
                                            repid="IDL:BULKIO/dataSDDS:1.0",
                                            type_="data",)

        port_dataCharOut = usesport(name="dataCharOut",
                                            repid="IDL:BULKIO/dataChar:1.0",
                                            type_="control",)

        port_dataOctetOut = usesport(name="dataOctetOut",
                                            repid="IDL:BULKIO/dataOctet:1.0",
                                            type_="control",)

        port_dataShortOut = usesport(name="dataShortOut",
                                            repid="IDL:BULKIO/dataShort:1.0",
                                            type_="control",)

        port_dataUShortOut = usesport(name="dataUShortOut",
                                            repid="IDL:BULKIO/dataUshort:1.0",
                                            type_="control",)

        port_dataLongOut = usesport(name="dataLongOut",
                                            repid="IDL:BULKIO/dataLong:1.0",
                                            type_="control",)

        port_dataULongOut = usesport(name="dataULongOut",
                                            repid="IDL:BULKIO/dataUlong:1.0",
                                            type_="control",)

        port_dataLongLongOut = usesport(name="dataLongLongOut",
                                            repid="IDL:BULKIO/dataLongLong:1.0",
                                            type_="control",)

        port_dataULongLongOut = usesport(name="dataULongLongOut",
                                            repid="IDL:BULKIO/dataUlongLong:1.0",
                                            type_="control",)

        port_dataFloatOut = usesport(name="dataFloatOut",
                                            repid="IDL:BULKIO/dataFloat:1.0",
                                            type_="control",)

        port_dataDoubleOut = usesport(name="dataDoubleOut",
                                            repid="IDL:BULKIO/dataDouble:1.0",
                                            type_="control",)

        port_dataFileOut = usesport(name="dataFileOut",
                                            repid="IDL:BULKIO/dataFile:1.0",
                                            type_="control",)

        port_dataXMLOut = usesport(name="dataXMLOut",
                                            repid="IDL:BULKIO/dataXML:1.0",
                                            type_="control",)

        port_dataSDDSOut = usesport(name="dataSDDSOut",
                                            repid="IDL:BULKIO/dataSDDS:1.0",
                                            type_="control",)

        port_propEvent = usesport(name="propEvent",
                                            repid="IDL:omg.org/CosEventChannelAdmin/EventChannel:1.0",
                                            type_="responses",)


from prg_adversary import adversary_G
from prg_adversary import adversary_rand
import matplotlib.pyplot as plt

# Run each function multiple times and collect the results.
def compare_distributions(num_trials=1000):
    results_G = [adversary_G() for _ in range(num_trials)]
    results_rand = [adversary_rand() for _ in range(num_trials)]

    # Calculate True/False ratios, handling the case where the denominator might be zero.
    ratio_G = (results_G.count(True) / results_G.count(False)) if results_G.count(False) > 0 else float('inf')
    ratio_rand = (results_rand.count(True) / results_rand.count(False)) if results_rand.count(False) > 0 else float('inf')

    return {
        'adversary_G_True_False_Ratio': ratio_G,
        'adversary_rand_True_False_Ratio': ratio_rand,
        'results_G': results_G,
        'results_rand': results_rand
    }

# Compare the distributions
compare_distributions_result = compare_distributions()

# Extract the results from the previous run for both functions
results_G = compare_distributions_result['results_G']
results_rand = compare_distributions_result['results_rand']

# Count the True and False occurrences for both functions
true_false_counts_G = [results_G.count(True), results_G.count(False)]
true_false_counts_rand = [results_rand.count(True), results_rand.count(False)]

# Set up the figure and axes for the bar plot
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 5))

# Titles for subplots
axes[0].set_title('Adversary G Distribution')
axes[1].set_title('Adversary Random Distribution')

# Labels for bars
labels = ['True', 'False']

# Plotting the True/False distribution for adversary_G
axes[0].bar(labels, true_false_counts_G, color=['blue', 'orange'])
axes[0].set_ylim(0, max(true_false_counts_G + true_false_counts_rand) * 1.1)

# Plotting the True/False distribution for adversary_rand
axes[1].bar(labels, true_false_counts_rand, color=['blue', 'orange'])
axes[1].set_ylim(0, max(true_false_counts_G + true_false_counts_rand) * 1.1)

# Display the plot
plt.tight_layout()
plt.show()


# TODO: global session
import asyncio
import logging
import os
import time

import aiohttp

from ..utils import async_lru_cache

try:
    TOKEN = os.environ['RIOT_TOKEN']
except KeyError:
    try:
        TOKEN = open('/run/secrets/riot_token').read().rstrip()
    except IOError:
        TOKEN = None
API_HEADERS = {'X-Riot-Token': TOKEN}

API_STATIC_DATA = 'https://na1.api.riotgames.com/lol/static-data/v3'
API_CHAMP = API_STATIC_DATA + '/champions/{}?champData=all'
API_CHAMPS = API_STATIC_DATA + '/champions'
API_ITEM = API_STATIC_DATA + '/items/{}?itemData=all'
API_ITEMS = API_STATIC_DATA + '/items'

logger = logging.getLogger()

class RateLimiter:
    INTERVAL = 10
    TOKENS = 250  # Riot gives us 500 per 10s, but lets play it safe

    def __init__(self):
        self.tokens = self.TOKENS
        self.updated_at = time.monotonic()

    def add_new_tokens(self):
        now = time.monotonic()
        if now - self.updated_at >= self.INTERVAL:
            self.updated_at = now
            self.tokens = self.TOKENS

    async def wait_for_token(self):
        while self.tokens < 1:
            self.add_new_tokens()
            await asyncio.sleep(1)

        self.tokens -= 1

    async def get(self, client, url, *args, **kwargs):
        await self.wait_for_token()
        response = await client.get(url, *args, **kwargs)
        if response.status == 429:
            logger.info('got 429 accessing %s', url)
            self.tokens = 0

            try:
                retry_after = int(response.headers['Retry-After'])
                logger.info('retrying %s in %ds', url, retry_after)
                await asyncio.sleep(retry_after)
                return await self.get(client, url, *args, **kwargs)
            except (KeyError, ValueError):
                logger.warning('bad Retry-After, got %s', response.headers)

        return response

LIMITER = RateLimiter()

@async_lru_cache(maxsize=256)
async def get_champ(cid):
    assert TOKEN
    logger.debug('get_champ(%s)', cid)
    url = API_CHAMP.format(cid)
    async with aiohttp.ClientSession() as client:
        response = await LIMITER.get(client, url, headers=API_HEADERS)
        assert response.status == 200
        return await response.json()

@async_lru_cache(maxsize=2)
async def get_champs():
    assert TOKEN
    logger.debug('get_champs()')
    url = API_CHAMPS
    async with aiohttp.ClientSession() as client:
        response = await LIMITER.get(client, url, headers=API_HEADERS)
        assert response.status == 200
        return await response.json()

@async_lru_cache(maxsize=256)
async def get_item(iid):
    assert TOKEN
    logger.debug('get_item(%s)', iid)
    url = API_ITEM.format(iid)
    async with aiohttp.ClientSession() as client:
        response = await LIMITER.get(client, url, headers=API_HEADERS)
        assert response.status == 200
        return await response.json()

@async_lru_cache(maxsize=2)
async def get_items():
    assert TOKEN
    logger.debug('get_items()')
    url = API_ITEMS
    async with aiohttp.ClientSession() as client:
        response = await LIMITER.get(client, url, headers=API_HEADERS)
        assert response.status == 200
        return await response.json()

async def reset_cache():
    while True:
        get_champ.cache_clear()
        get_champs.cache_clear()
        get_item.cache_clear()
        get_items.cache_clear()

        try:
            tasks = []
            for iid in (await get_items())['data']:
                tasks.append(asyncio.ensure_future(get_item(int(iid))))

            await asyncio.gather(*tasks)
            logger.debug('riot cache refreshed')
        except Exception as e:  # pylint: disable=broad-except
            logger.error('error refreshing riot cache')
            logger.exception(e)

        await asyncio.sleep(60 * 60 * 24)


import unittest

from text_cleaning.functions.text import ReplaceEitherSlashWithSpace

class TestReplaceEitherSlashWithSpace(unittest.TestCase):

    def test_case_1(self):
        replace = ReplaceEitherSlashWithSpace()
        text = 'Something/nothing whatever.'
        result = replace(text)
        expected = 'Something nothing whatever.'
        self.assertEqual(expected, result)


class Employee:
    no_of_leaves=8

    # for  using constructor  we have to use def__init__() function

    def __init__(self,aname,asalary,arole):
        self.name=aname
        self.salary=asalary
        self.role=arole

    # use of self

    def print_details(self):    # so self is used for taking object as a argument
        return f'Name is {self.name}, salary is {self.salary}, role is {self.role}'

#harry=Employee()
harry=Employee('Harry','45000','manager')
#harpal=Employee()

# harry.name='Harry'
# harry.salary=45000
# harry.role='manager'
#
# harpal.name='Harpal'
# harpal.salary=55000
# harpal.role='senior manager'
# print(harry.role)
#
# print(harry.print_details())  # in this case of object is harry and is passed

# use of constructor in python is done using
# def __init__()  using this function

print(harry.salary)   # this is printing the output using constructor


# Jesus Carlos Martinez Gonzalez
# 17/08/2023
# The cheaper cab (https://www.codechef.com/problems/CABS)

"""
Chef has to travel to another place. For this, he can avail any one of two cab services.
• The first cab service charges X rupees.
• The second cab service charges Y rupees.
Chef wants to spend the minimum amount of money. Which cab service should Chef take?
"""

for _ in range(int(input())):
    a, b = map(int, input().split())
    if a < b:
        print("FIRST")
    elif a > b:
        print("SECOND")
    else:
        print("ANY")


"""Functions related to loss calculation
"""
import torch

def get_loss_weights(config):
    weight_list = list(config["logic"]["training"]["loss"]["weights"])
    weights = []
    for seg_class in weight_list :
        weights.append(config["logic"]["training"]["loss"]["weights"][seg_class])
    weight_tensor = torch.Tensor(weights)
    return weight_tensor

def get_loss(function, weights):
    if function == "CE":
        criterion = torch.nn.CrossEntropyLoss(weight=weights, reduction='mean')
    else :
        print("Not implemented loss.")
        raise
    return criterion

def calculate_loss(criterion, output, target):
    loss = criterion(output, target)
    return loss

def print_losses(epoch, train_loss, valid_loss):
    print("Epoch " + str(epoch) + "  :  train_Loss=" + str(train_loss) + "    val_Loss=" + str(valid_loss))