package me.despawningbone.discordbot.command.games;

import java.util.ArrayList;
import java.util.Collections;
import java.io.BufferedReader;
import java.io.IOException;

//edited by me for compatibility / bug fixes
//TODO clean up koohii to fit my own code style? rn it feels really awkward to use ngl

/**
* pure java implementation of github.com/Francesco149/oppai-ng .
*
* <blockquote><pre>
* this is meant to be a single file library that's as portable and
* easy to set up as possible for java projects that need
* pp/difficulty calculation.
*
* when running the test suite, speed is roughly equivalent to the C
* implementation, but peak memory usage is almost 80 times higher.
* if you are on a system with limited resources or you don't want
* to spend time installing and setting up java, you can use the C
* implementation which doesn't depend on any third party software.
* -----------------------------------------------------------------
* usage:
* put Koohii.java in your project's folder
* -----------------------------------------------------------------
* import java.io.BufferedReader;
* import java.io.InputStreamReader;
*
* class Example {
*
* public static void main(String[] args) throws java.io.IOException
* {
*     BufferedReader stdin =
*         new BufferedReader(new InputStreamReader(System.in)
*     );
*
*     Koohii.Map beatmap = new Koohii.Parser().map(stdin);
*     Koohii.DiffCalc stars = new Koohii.DiffCalc().calc(beatmap);
*     System.out.printf("%s stars\n", stars.total);
*
*     Koohii.PPv2 pp = Koohii.PPv2(
*         stars.aim, stars.speed, beatmap
*     );
*
*     System.out.printf("%s pp\n", pp.total);
* }
*
* }
* -----------------------------------------------------------------
* javac Example.java
* cat /path/to/file.osu | java Example
* -----------------------------------------------------------------
* this is free and unencumbered software released into the
* public domain.
*
* refer to the attached UNLICENSE or http://unlicense.org/
* </pre></blockquote>
*
* @author Franc[e]sco (lolisamurai@tfwno.gf)
*/
public final class Koohii {

private Koohii() {}

public final int VERSION_MAJOR = 2;
public final int VERSION_MINOR = 1;
public final int VERSION_PATCH = 0;

/** prints a message to stderr. */
public static
void info(String fmt, Object... args) {
    System.err.printf(fmt, args);
}

/* ------------------------------------------------------------- */
/* math                                                          */

/** 2D vector with double values */
public static class Vector2
{
    public double x = 0.0, y = 0.0;

    public Vector2() {}
    public Vector2(Vector2 other) { this(other.x, other.y); }
    public Vector2(double x, double y) { this.x = x; this.y = y; }

    public String toString() {
        return String.format("(%s, %s)", x, y);
    }

    /**
    * this -= other .
    * @return this
    */
    public Vector2 sub(Vector2 other)
    {
        x -= other.x; y -= other.y;
        return this;
    }

    /**
    * this *= value .
    * @return this
    */
    public Vector2 mul(double value)
    {
        x *= value; y *= value;
        return this;
    }

    /** length (magnitude) of the vector. */
    public double len() { return Math.sqrt(x * x + y * y); }

    /** dot product between two vectors, correlates with the angle */
    public double dot(Vector2 other) { return x * other.x + y * other.y; }
}

/* ------------------------------------------------------------- */
/* beatmap utils                                                 */

public static final int MODE_STD = 0;
public static final int MODE_TK = 1;

public static class Circle
{
    public Vector2 pos = new Vector2();
    public String toString() { return pos.toString(); }
}

public static class Slider
{
    public Vector2 pos = new Vector2();

    /** distance travelled by one repetition. */
    public double distance = 0.0;

    /** 1 = no repeats. */
    public int repetitions = 1;

    public String toString()
    {
        return String.format(
            "{ pos=%s, distance=%s, repetitions=%d }",
            pos, distance, repetitions
        );
    }
}

public static final int OBJ_CIRCLE = 1<<0;
public static final int OBJ_SLIDER = 1<<1;
public static final int OBJ_SPINNER = 1<<3;

/** strain index for speed */
public final static int DIFF_SPEED = 0;

/** strain index for aim */
public final static int DIFF_AIM = 1;

public static class HitObject
{
    /** start time in milliseconds. */
    public double time = 0.0;
    public int type = OBJ_CIRCLE;

    /** an instance of Circle or Slider or null. */
    public Object data = null;
    public Vector2 normpos =  new Vector2();
    public double angle = 0.0;
    public final double[] strains = new double[] { 0.0, 0.0 };
    public boolean is_single = false;
    public double delta_time = 0.0;
    public double d_distance = 0.0;

    /** string representation of the type bitmask. */
    public String typestr()
    {
        StringBuilder res = new StringBuilder();

        if ((type & OBJ_CIRCLE) != 0) res.append("circle | ");
        if ((type & OBJ_SLIDER) != 0) res.append("slider | ");
        if ((type & OBJ_SPINNER) != 0) res.append("spinner | ");

        String result = res.toString();
        return result.substring(0, result.length() - 3);
    }

    public String toString()
    {
        return String.format(
            "{ time=%s, type=%s, data=%s, normpos=%s, " +
            "strains=[ %s, %s ], is_single=%s }",
            time, typestr(), data, normpos, strains[0], strains[1],
            is_single
        );
    }
}

public static class Timing
{
    /** start time in milliseconds. */
    public double time = 0.0;
    public double ms_per_beat = -100.0;

    /** if false, ms_per_beat is -100 * bpm_multiplier. */
    public boolean change = false;
}

/**
* the bare minimum beatmap data for difficulty calculation.
*
* this object can be reused for multiple beatmaps without
* re-allocation by simply calling reset()
*/
public static class Map
{
    public int format_version;
    public int mode;
    public String title, title_unicode;
    public String artist, artist_unicode;

    /** mapper name. */
    public String creator;

    /** difficulty name. */
    public String version;

    public int ncircles, nsliders, nspinners;
    public float hp, cs, od, ar;
    public float sv, tick_rate;

    public final ArrayList<HitObject> objects =
        new ArrayList<HitObject>(512);

    public final ArrayList<Timing> tpoints =
        new ArrayList<Timing>(32);

    public Map() { reset(); }

    /** clears the instance so that it can be reused. */
    public void reset()
    {
        title = title_unicode =
        artist = artist_unicode =
        creator =
        version = "";

        ncircles = nsliders = nspinners = 0;
        hp = cs = od = ar = 5.0f;
        sv = tick_rate = 1.0f;

        objects.clear(); tpoints.clear();
    }

    public String toString()
    {
        StringBuilder sb = new StringBuilder();

        for (HitObject obj : objects)
        {
            sb.append(obj);
            sb.append(", ");
        }

        String objs_str = sb.toString();

        sb.setLength(0);

        for (Timing t : tpoints)
        {
            sb.append(t);
            sb.append(", ");
        }

        String timing_str = sb.toString();

        return String.format(
            "beatmap { mode=%d, title=%s, title_unicode=%s, " +
            "artist=%s, artist_unicode=%s, creator=%s, " +
            "version=%s, ncircles=%d, nsliders=%d, nspinners=%d," +
            " hp=%s, cs=%s, od=%s, ar=%s, sv=%s, tick_rate=%s, " +
            "tpoints=[ %s ], objects=[ %s ] }",
            mode, title, title_unicode, artist, artist_unicode,
            creator, version, ncircles, nsliders, nspinners, hp,
            cs, od, ar, sv, tick_rate, timing_str, objs_str
        );
    }

    //CUSTOM: added taiko handling method which is way less complicated than this
    public int max_combo()
    {
		int res = 0;
    	switch(mode) {
    	case MODE_STD:
            int tindex = -1;
            double tnext = Double.NEGATIVE_INFINITY;
            double px_per_beat = 0.0;

            for (HitObject obj : objects)
            {
                if ((obj.type & OBJ_SLIDER) == 0)
                {
                    /* non-sliders add 1 combo */
                    ++res;
                    continue;
                }

                /* keep track of the current timing point without
                looping through all of them for every object */
                while (obj.time >= tnext)
                {
                    ++tindex;

                    if (tpoints.size() > tindex + 1) {
                        tnext = tpoints.get(tindex + 1).time;
                    } else {
                        tnext = Double.POSITIVE_INFINITY;
                    }

                    Timing t = tpoints.get(tindex);

                    double sv_multiplier = 1.0;

                    if (!t.change && t.ms_per_beat < 0) {
                        sv_multiplier = -100.0 / t.ms_per_beat;
                    }

                    px_per_beat = sv * 100.0 * sv_multiplier;
                    if (format_version < 8) {
                        px_per_beat /= sv_multiplier;
                    }
                }

                /* slider, we need to calculate slider ticks */
                Slider sl = (Slider)obj.data;

                double num_beats =
                    (sl.distance * sl.repetitions) / px_per_beat;

                int ticks = (int)
                    Math.ceil(
                        (num_beats - 0.1) / sl.repetitions * tick_rate
                    );

                --ticks;
                ticks *= sl.repetitions;
                ticks += sl.repetitions + 1;

                res += Math.max(0, ticks);
            }
            break;
    	case MODE_TK:
    		res = ncircles;
    		break;
    	default:  //should never throw since this object cant get past init if its unsupported
    		throw new UnsupportedOperationException("unsupported gamemode");
    	}

        return res;
    }
}

/* ------------------------------------------------------------- */
/* beatmap parser                                                */

/* note: I just let parser throw built-in exceptions instead of
error checking stuff because it's as good as making my own
exception since you can check lastline/lastpos when you catch */

public static class Parser
{
    /** last line touched. */
    public String lastline;

    /** last line number touched. */
    public int nline;

    /** last token touched. */
    public String lastpos;

    /** true if the parsing completed successfully. */
    public boolean done;

    /**
    * the parsed beatmap will be stored in this object.
    * willl persist throughout reset() calls and will be reused by
    * subsequent parse calls until changed.
    * @see Parser#reset
    */
    public Map beatmap = null;

    private String section; /* current section */
    private boolean ar_found = false;

    public Parser() { reset(); }

    private void reset()
    {
        lastline = lastpos = section = "";
        nline = 0;
        done = false;
        if (beatmap != null) {
            beatmap.reset();
        }
    }

    public String toString()
    {
        return String.format(
            "in line %d\n%s\n> %s", nline, lastline, lastpos
        );
    }

    private void warn(String fmt, Object... args)
    {
        info("W: ");
        info(fmt, args);
        info("\n%s\n", this);
    }

    /**
    * trims v, sets lastpos to it and returns trimmed v.
    * should be used to access any string that can make the parser
    * fail
    */
    private String setlastpos(String v)
    {
        v = v.trim();
        lastpos = v;
        return v;
    }

    private String[] property()
    {
        String[] split = lastline.split(":", 2);
        split[0] = setlastpos(split[0]);
        if (split.length > 1) {
            split[1] = setlastpos(split[1]);
        }
        /* why does java have such inconsistent naming? ArrayList
        length is .size(), normal array length is .length, string
        length is .length(). why do I have to look up documentation
        for stuff that should have the same interface? */
        return split;
    }

    private void metadata()
    {
        String[] p = property();

        if (p[0].equals("Title")) {
            beatmap.title = p[1];
        }
        else if (p[0].equals("TitleUnicode")) {
            beatmap.title_unicode = p[1];
        }
        else if (p[0].equals("Artist")) {
            beatmap.artist = p[1];
        }
        else if (p[0].equals("ArtistUnicode")) {
            beatmap.artist_unicode = p[1];
        }
        else if (p[0].equals("Creator")) {
            beatmap.creator = p[1];
        }
        else if (p[0].equals("Version")) {
            beatmap.version = p[1];
        }
    }

    private void general()
    {
        String[] p = property();

        if (p[0].equals("Mode"))
        {
            beatmap.mode = Integer.parseInt(setlastpos(p[1]));

            if (beatmap.mode != MODE_STD && beatmap.mode != MODE_TK)
            {
                throw new UnsupportedOperationException(
                    "this gamemode is not yet supported"
                );
            }
        }
    }

    private void difficulty()
    {
        String[] p = property();

        /* what's up with the redundant Float.parseFloat ?_? */
        if (p[0].equals("CircleSize")) {
            beatmap.cs = Float.parseFloat(setlastpos(p[1]));
        }
        else if (p[0].equals("OverallDifficulty")) {
            beatmap.od = Float.parseFloat(setlastpos(p[1]));
        }
        else if (p[0].equals("ApproachRate")) {
            beatmap.ar = Float.parseFloat(setlastpos(p[1]));
            ar_found = true;
        }
        else if (p[0].equals("HPDrainRate")) {
            beatmap.hp = Float.parseFloat(setlastpos(p[1]));
        }
        else if (p[0].equals("SliderMultiplier")) {
            beatmap.sv = Float.parseFloat(setlastpos(p[1]));
        }
        else if (p[0].equals("SliderTickRate")) {
            beatmap.tick_rate = Float.parseFloat(setlastpos(p[1]));
        }
    }

    private void timing()
    {
        String[] s = lastline.split(",");

        if (s.length > 8) {
            warn("timing point with trailing values");
        }

        Timing t = new Timing();
        t.time = Double.parseDouble(setlastpos(s[0]));
        t.ms_per_beat = Double.parseDouble(setlastpos(s[1]));

        if (s.length >= 7) {
            t.change = !s[6].trim().equals("0");
        }

        beatmap.tpoints.add(t);
    }

    private void objects()
    {
        String[] s = lastline.split(",");

        if (s.length > 11) {
            warn("object with trailing values");
        }

        HitObject obj = new HitObject();
        obj.time = Double.parseDouble(setlastpos(s[2]));
        obj.type = Integer.parseInt(setlastpos(s[3]));

        if ((obj.type & OBJ_CIRCLE) != 0)
        {
            ++beatmap.ncircles;
            Circle c = new Circle();
            c.pos.x = Double.parseDouble(setlastpos(s[0]));
            c.pos.y = Double.parseDouble(setlastpos(s[1]));
            obj.data = c;
        }

        else if ((obj.type & OBJ_SPINNER) != 0) {
            ++beatmap.nspinners;
        }

        else if ((obj.type & OBJ_SLIDER) != 0)
        {
            ++beatmap.nsliders;
            Slider sli = new Slider();
            sli.pos.x = Double.parseDouble(setlastpos(s[0]));
            sli.pos.y = Double.parseDouble(setlastpos(s[1]));
            sli.repetitions = Integer.parseInt(setlastpos(s[6]));
            sli.distance = Double.parseDouble(setlastpos(s[7]));
            obj.data = sli;
        }

        beatmap.objects.add(obj);
    }

    /**
    * calls reset() on beatmap and parses a osu file into it.
    * if beatmap is null, it will be initialized to a new Map
    * @return this.beatmap
    * @throws IOException
    */
    public Map map(BufferedReader reader) throws IOException
    {
        String line = null;

        if (beatmap == null) {
            beatmap = new Map();
        }

        reset();

        while ((line = reader.readLine()) != null)
        {
            lastline = line;
            ++nline;

            /* comments (according to lazer) */
            if (line.startsWith(" ") || line.startsWith("_")) {
                continue;
            }

            line = lastline = line.trim();
            if (line.length() <= 0) {
                continue;
            }

            /* c++ style comments */
            if (line.startsWith("//")) {
                continue;
            }

            /* [SectionName] */
            if (line.startsWith("[")) {
                section = line.substring(1, line.length() - 1);
                continue;
            }

            try
            {
                if (section.equals("Metadata"))
                    metadata();
                else if (section.equals("General"))
                    general();
                else if (section.equals("Difficulty"))
                    difficulty();
                else if (section.equals("TimingPoints"))
                    timing();
                else if (section.equals("HitObjects"))
                    objects();
                else {
                    int fmt_index = line.indexOf("file format v");
                    if (fmt_index < 0) {
                        continue;
                    }

                    beatmap.format_version = Integer.parseInt(
                        line.substring(fmt_index + 13)
                    );
                }
            }

            catch (NumberFormatException e) {
                warn("ignoring line with bad number");
            } catch (ArrayIndexOutOfBoundsException e) {
                warn("ignoring malformed line");
            }
        }

        if (!ar_found) {
            beatmap.ar = beatmap.od;
        }

        done = true;
        return beatmap;
    }

    /**
    * sets beatmap and returns map(reader)
    * @return this.beatmap
    * @throws IOException
    */
    public Map map(BufferedReader reader, Map beatmap)
        throws IOException
    {
        this.beatmap = beatmap;
        return map(reader);
    }
}

/* ------------------------------------------------------------- */
/* mods utils                                                    */

public static final int MODS_NOMOD = 0;

public static final int MODS_NF = 1<<0;
public static final int MODS_EZ = 1<<1;
public static final int MODS_TOUCH_DEVICE = 1<<2;
public static final int MODS_TD = MODS_TOUCH_DEVICE;
public static final int MODS_HD = 1<<3;
public static final int MODS_HR = 1<<4;
public static final int MODS_DT = 1<<6;
public static final int MODS_HT = 1<<8;
public static final int MODS_NC = 1<<9;
public static final int MODS_FL = 1<<10;
public static final int MODS_SO = 1<<12;

public static final int MODS_SPEED_CHANGING =
    MODS_DT | MODS_HT | MODS_NC;

public static final int MODS_MAP_CHANGING =
    MODS_HR | MODS_EZ | MODS_SPEED_CHANGING;

/** @return a string representation of the mods, such as HDDT */
public static
String mods_str(int mods)
{
    StringBuilder sb = new StringBuilder();

    if ((mods & MODS_NF) != 0) {
        sb.append("NF");
    }

    if ((mods & MODS_EZ) != 0) {
        sb.append("EZ");
    }

    if ((mods & MODS_TOUCH_DEVICE) != 0) {
        sb.append("TD");
    }

    if ((mods & MODS_HD) != 0) {
        sb.append("HD");
    }

    if ((mods & MODS_HR) != 0) {
        sb.append("HR");
    }

    if ((mods & MODS_NC) != 0) {
        sb.append("NC");
    } else if ((mods & MODS_DT) != 0) {
        sb.append("DT");
    }

    if ((mods & MODS_HT) != 0) {
        sb.append("HT");
    }

    if ((mods & MODS_FL) != 0) {
        sb.append("FL");
    }

    if ((mods & MODS_SO) != 0) {
        sb.append("SO");
    }

    return sb.toString();
}

/** @return mod bitmask from the string representation */
public static
int mods_from_str(String str)
{
    int mask = 0;

    while (str.length() > 0)
    {
        if (str.startsWith("NF")) mask |= MODS_NF;
        else if (str.startsWith("EZ")) mask |= MODS_EZ;
        else if (str.startsWith("TD")) mask |= MODS_TOUCH_DEVICE;
        else if (str.startsWith("HD")) mask |= MODS_HD;
        else if (str.startsWith("HR")) mask |= MODS_HR;
        else if (str.startsWith("DT")) mask |= MODS_DT;
        else if (str.startsWith("HT")) mask |= MODS_HT;
        else if (str.startsWith("NC")) mask |= MODS_NC;
        else if (str.startsWith("FL")) mask |= MODS_FL;
        else if (str.startsWith("SO")) mask |= MODS_SO;
        else {
            str = str.substring(1);
            continue;
        }
        str = str.substring(2);
    }

    return mask;
}

/**
* beatmap stats with mods applied.
* should be populated with the base beatmap stats and passed to
* mods_apply which will modify the stats for the given mods
*/
public static class MapStats
{
    float ar, od, cs, hp;
    
    public MapStats() {
    	
    }
    
    public MapStats(Map map) {
    	ar = map.ar;
    	od = map.od;
    	cs = map.cs;
    	hp = map.hp;
    }

    /**
    * speed multiplier / music rate.
    * this doesn't need to be initialized before calling mods_apply
    */
    float speed = 1.0f;
}

private static final double OD0_MS = 80;
private static final double OD10_MS = 20;
private static final double AR0_MS = 1800.0;
private static final double AR5_MS = 1200.0;
private static final double AR10_MS = 450.0;

private static final double OD_MS_STEP = (OD0_MS - OD10_MS) / 10.0;
private static final double AR_MS_STEP1 = (AR0_MS - AR5_MS) / 5.0;
private static final double AR_MS_STEP2 = (AR5_MS - AR10_MS) / 5.0;

private static final int APPLY_AR = 1<<0;
private static final int APPLY_OD = 1<<1;
private static final int APPLY_CS = 1<<2;
private static final int APPLY_HP = 1<<3;

/**
* applies mods to mapstats.
*
* <blockquote><pre>
* Koohii.MapStats mapstats = new Koohii.MapStats();
* mapstats.ar = 9;
* Koohii.mods_apply(Koohii.MODS_DT, mapstats, Koohii.APPLY_AR);
* // mapstats.ar is now 10.33, mapstats.speed is 1.5
* </pre></blockquote>
*
* @param mapstats the base beatmap stats
* @param flags bitmask that specifies which stats to modify. only
*              the stats specified here need to be initialized in
*              mapstats.
* @return mapstats
* @see MapStats
*/
public static
MapStats mods_apply(int mods, MapStats mapstats, int flags)
{
    mapstats.speed = 1.0f;

    if ((mods & MODS_MAP_CHANGING) == 0) {
        return mapstats;
    }

    if ((mods & (MODS_DT | MODS_NC)) != 0) {
        mapstats.speed = 1.5f;
    }

    if ((mods & MODS_HT) != 0) {
        mapstats.speed *= 0.75f;
    }

    float od_ar_hp_multiplier = 1.0f;

    if ((mods & MODS_HR) != 0) {
        od_ar_hp_multiplier = 1.4f;
    }

    if ((mods & MODS_EZ) != 0) {
        od_ar_hp_multiplier *= 0.5f;
    }

    if ((flags & APPLY_AR) != 0)
    {
        mapstats.ar *= od_ar_hp_multiplier;

        /* convert AR into milliseconds window */
        double arms = mapstats.ar < 5.0f ?
            AR0_MS - AR_MS_STEP1 * mapstats.ar
            : AR5_MS - AR_MS_STEP2 * (mapstats.ar - 5.0f);

        /* stats must be capped to 0-10 before HT/DT which brings
        them to a range of -4.42->11.08 for OD and -5->11 for AR */
        arms = Math.min(AR0_MS, Math.max(AR10_MS, arms));
        arms /= mapstats.speed;

        mapstats.ar = (float)(
            arms > AR5_MS ?
            (AR0_MS - arms) / AR_MS_STEP1
            : 5.0 + (AR5_MS - arms) / AR_MS_STEP2
        );
    }

    if ((flags & APPLY_OD) != 0)
    {
        mapstats.od *= od_ar_hp_multiplier;
        double odms = OD0_MS - Math.ceil(OD_MS_STEP * mapstats.od);
        odms = Math.min(OD0_MS, Math.max(OD10_MS, odms));
        odms /= mapstats.speed;
        mapstats.od = (float)((OD0_MS - odms) / OD_MS_STEP);
    }

    if ((flags & APPLY_CS) != 0)
    {
        if ((mods & MODS_HR) != 0) {
            mapstats.cs *= 1.3f;
        }

        if ((mods & MODS_EZ) != 0) {
            mapstats.cs *= 0.5f;
        }

        mapstats.cs = Math.min(10.0f, mapstats.cs);
    }

    if ((flags & APPLY_HP) != 0)
    {
        mapstats.hp =
            Math.min(10.0f, mapstats.hp * od_ar_hp_multiplier);
    }

    return mapstats;
}

/* ------------------------------------------------------------- */
/* difficulty calculator                                         */

/**
* arbitrary thresholds to determine when a stream is spaced
* enough that it becomes hard to alternate.
*/
private final static double SINGLE_SPACING = 125.0;

/** strain decay per interval. */
private final static double[] DECAY_BASE = { 0.3, 0.15 };

/** balances speed and aim. */
private final static double[] WEIGHT_SCALING = { 1400.0, 26.25 };

/**
* max strains are weighted from highest to lowest, this is how
* much the weight decays.
*/
private final static double DECAY_WEIGHT = 0.9;

/**
* strains are calculated by analyzing the map in chunks and taking
* the peak strains in each chunk. this is the length of a strain
* interval in milliseconds
*/
private final static double STRAIN_STEP = 400.0;

/** non-normalized diameter where the small circle buff starts. */
private final static double CIRCLESIZE_BUFF_THRESHOLD = 30.0;

/** global stars multiplier. */
private final static double STAR_SCALING_FACTOR = 0.0675;

/** in osu! pixels */
private final static double PLAYFIELD_WIDTH = 512.0,
                            PLAYFIELD_HEIGHT = 384.0;

private final static Vector2 PLAYFIELD_CENTER = new Vector2(
    PLAYFIELD_WIDTH / 2.0, PLAYFIELD_HEIGHT / 2.0
);

/**
* 50% of the difference between aim and speed is added to total
* star rating to compensate for aim/speed only maps
*/
private final static double EXTREME_SCALING_FACTOR = 0.5;

private final static double MIN_SPEED_BONUS = 75.0;
private final static double MAX_SPEED_BONUS = 45.0;
private final static double ANGLE_BONUS_SCALE = 90.0;
private final static double AIM_TIMING_THRESHOLD = 107;
private final static double SPEED_ANGLE_BONUS_BEGIN = 5 * Math.PI / 6;
private final static double AIM_ANGLE_BONUS_BEGIN = Math.PI / 3;

private static
double d_spacing_weight(int type, double distance, double delta_time,
    double prev_distance, double prev_delta_time, double angle)
{
    double strain_time = Math.max(delta_time, 50.0);
    double prev_strain_time = Math.max(prev_delta_time, 50.0);
    double angle_bonus;
    switch (type)
    {
      case DIFF_AIM: {
          double result = 0.0;
          if (!Double.isNaN(angle) && angle > AIM_ANGLE_BONUS_BEGIN) {
              angle_bonus = Math.sqrt(
                  Math.max(prev_distance - ANGLE_BONUS_SCALE, 0.0) *
                  Math.pow(Math.sin(angle - AIM_ANGLE_BONUS_BEGIN), 2.0) *
                  Math.max(distance - ANGLE_BONUS_SCALE, 0.0)
              );
              result = (
                  1.5 * Math.pow(Math.max(0.0, angle_bonus), 0.99) /
                  Math.max(AIM_TIMING_THRESHOLD, prev_strain_time)
              );
          }
          double weighted_distance = Math.pow(distance, 0.99);
          return Math.max(result +
              weighted_distance /
              Math.max(AIM_TIMING_THRESHOLD, strain_time),
              weighted_distance / strain_time);
      }

      case DIFF_SPEED: {
          distance = Math.min(distance, SINGLE_SPACING);
          delta_time = Math.max(delta_time, MAX_SPEED_BONUS);
          double speed_bonus = 1.0;
          if (delta_time < MIN_SPEED_BONUS) {
              speed_bonus +=
                Math.pow((MIN_SPEED_BONUS - delta_time) / 40.0, 2);
          }
          angle_bonus = 1.0;
          if (!Double.isNaN(angle) && angle < SPEED_ANGLE_BONUS_BEGIN) {
              double s = Math.sin(1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle));
              angle_bonus += Math.pow(s, 2) / 3.57;
              if (angle < Math.PI / 2.0) {
                  angle_bonus = 1.28;
                  if (distance < ANGLE_BONUS_SCALE && angle < Math.PI / 4.0) {
                      angle_bonus += (1.0 - angle_bonus) *
                          Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0);
                  } else if (distance < ANGLE_BONUS_SCALE) {
                      angle_bonus += (1.0 - angle_bonus) *
                          Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0) *
                          Math.sin((Math.PI / 2.0 - angle) * 4.0 / Math.PI);
                  }
              }
          }
          return (
              (1 + (speed_bonus - 1) * 0.75) * angle_bonus *
              (0.95 + speed_bonus * Math.pow(distance / SINGLE_SPACING, 3.5))
          ) / strain_time;
      }
    }

    throw new UnsupportedOperationException(
        "this difficulty type does not exist"
    );
}

/**
* calculates the strain for one difficulty type and stores it in
* obj. this assumes that normpos is already computed.
* this also sets is_single if type is DIFF_SPEED
*/
private static
void d_strain(int type, HitObject obj, HitObject prev,
    double speed_mul)
{
    double value = 0.0;
    double time_elapsed = (obj.time - prev.time) / speed_mul;
    double decay =
        Math.pow(DECAY_BASE[type], time_elapsed / 1000.0);

    obj.delta_time = time_elapsed;

    /* this implementation doesn't account for sliders */
    if ((obj.type & (OBJ_SLIDER | OBJ_CIRCLE)) != 0)
    {
        double distance =
            new Vector2(obj.normpos).sub(prev.normpos).len();
        obj.d_distance = distance;

        if (type == DIFF_SPEED) {
            obj.is_single = distance > SINGLE_SPACING;
        }

        value = d_spacing_weight(type, distance, time_elapsed,
            prev.d_distance, prev.delta_time, obj.angle);
        value *= WEIGHT_SCALING[type];
    }

    obj.strains[type] = prev.strains[type] * decay + value;
}

/**
* difficulty calculator, can be reused in subsequent calc() calls.
*/
public static class DiffCalc
{
    /** star rating. */
    public double total;

    /** aim stars. */
    public double aim;

    /** aim difficulty (used to calc length bonus) */
    public double aim_difficulty;

    /** aim length bonus (unused at the moment) */
    public double aim_length_bonus;

    /** speed stars. */
    public double speed;

    /** speed difficulty (used to calc length bonus) */
    public double speed_difficulty;

    /** speed length bonus (unused at the moment) */
    public double speed_length_bonus;

    /**
    * number of notes that are considered singletaps by the
    * difficulty calculator.
    */
    public int nsingles;

    /**
    * number of taps slower or equal to the singletap threshold
    * value.
    */
    public int nsingles_threshold;

    /**
    * the beatmap we want to calculate the difficulty for.
    * must be set or passed to calc() explicitly.
    * persists across calc() calls unless it's changed or explicity
    * passed to calc()
    * @see DiffCalc#calc(Koohii.Map, int, double)
    * @see DiffCalc#calc(Koohii.Map, int)
    * @see DiffCalc#calc(Koohii.Map)
    */
    public Map beatmap = null;

    private double speed_mul;
    private final ArrayList<Double> strains =
        new ArrayList<Double>(512);

    public DiffCalc() { reset(); }

    /** sets up the instance for re-use by resetting fields. */
    private void reset()
    {
        total = aim = speed = 0.0;
        nsingles = nsingles_threshold = 0;
        speed_mul = 1.0;
    }

    public String toString()
    {
        return String.format("%s stars (%s aim, %s speed)",
            total, aim, speed);
    }

    private static double length_bonus(double stars, double difficulty) {
        return (
            0.32 + 0.5 *
            (Math.log10(difficulty + stars) - Math.log10(stars))
        );
    }

    private class DiffValues
    {
        public double difficulty, total;

        public DiffValues(double difficulty, double total) {
          this.difficulty = difficulty;
          this.total = total;
        }
    };

    private DiffValues calc_individual(int type)
    {
        strains.clear();

        double strain_step = STRAIN_STEP * speed_mul;
        /* the first object doesn't generate a strain
         * so we begin with an incremented interval end */
        double interval_end = (
          Math.ceil(beatmap.objects.get(0).time / strain_step)
          * strain_step
        );
        double max_strain = 0.0;

        /* calculate all strains */
        for (int i = 0; i < beatmap.objects.size(); ++i)
        {
            HitObject obj = beatmap.objects.get(i);
            HitObject prev = i > 0 ?
                beatmap.objects.get(i - 1) : null;

            if (prev != null) {
                d_strain(type, obj, prev, speed_mul);
            }

            while (obj.time > interval_end)
            {
                /* add max strain for this interval */
                strains.add(max_strain);

                if (prev != null)
                {
                    /* decay last object's strains until the next
                    interval and use that as the initial max
                    strain */
                    double decay = Math.pow(DECAY_BASE[type],
                        (interval_end - prev.time) / 1000.0);

                    max_strain = prev.strains[type] * decay;
                } else {
                    max_strain = 0.0;
                }

                interval_end += strain_step;
            }

            max_strain = Math.max(max_strain, obj.strains[type]);
        }

        /* don't forget to add the last strain interval */
        strains.add(max_strain);

        /* weigh the top strains sorted from highest to lowest */
        double weight = 1.0;
        double total = 0.0;
        double difficulty = 0.0;

        Collections.sort(strains, Collections.reverseOrder());

        for (Double strain : strains)
        {
            total += Math.pow(strain, 1.2);
            difficulty += strain * weight;
            weight *= DECAY_WEIGHT;
        }

        return new DiffValues(difficulty, total);
    }

    /**
    * default value for singletap_threshold.
    * @see DiffCalc#calc
    */
    public final static double DEFAULT_SINGLETAP_THRESHOLD = 125.0;

    /**
    * calculates beatmap difficulty and stores it in total, aim,
    * speed, nsingles, nsingles_speed fields.
    * @param singletap_threshold the smallest milliseconds interval
    *        that will be considered singletappable. for example,
    *        125ms is 240 1/2 singletaps ((60000 / 240) / 2)
    * @return self
    */
    public DiffCalc calc(int mods, double singletap_threshold)
    {
        reset();

        MapStats mapstats = new MapStats();
        mapstats.cs = beatmap.cs;
        mods_apply(mods, mapstats, APPLY_CS);
        speed_mul = mapstats.speed;

        double radius = (PLAYFIELD_WIDTH / 16.0) *
            (1.0 - 0.7 * (mapstats.cs - 5.0) / 5.0);

        /* positions are normalized on circle radius so that we can
        calc as if everything was the same circlesize */
        double scaling_factor = 52.0 / radius;

        if (radius < CIRCLESIZE_BUFF_THRESHOLD)
        {
            scaling_factor *= 1.0 +
                Math.min(CIRCLESIZE_BUFF_THRESHOLD - radius, 5.0) / 50.0;
        }

        Vector2 normalized_center =
            new Vector2(PLAYFIELD_CENTER).mul(scaling_factor);

        HitObject prev1 = null;
        HitObject prev2 = null;
        int i = 0;

        /* calculate normalized positions */
        for (HitObject obj : beatmap.objects)
        {
            if ((obj.type & OBJ_SPINNER) != 0) {
                obj.normpos = new Vector2(normalized_center);
            }

            else
            {
                Vector2 pos;

                if ((obj.type & OBJ_SLIDER) != 0) {
                    pos = ((Slider)obj.data).pos;
                }

                else if ((obj.type & OBJ_CIRCLE) != 0) {
                    pos = ((Circle)obj.data).pos;
                }

                else
                {
                    info(
                        "W: unknown object type %08X\n",
                        obj.type
                    );
                    pos = new Vector2();
                }

                obj.normpos = new Vector2(pos).mul(scaling_factor);
            }

            if (i >= 2) {
                Vector2 v1 = new Vector2(prev2.normpos).sub(prev1.normpos);
                Vector2 v2 = new Vector2(obj.normpos).sub(prev1.normpos);
                double dot = v1.dot(v2);
                double det = v1.x * v2.y - v1.y * v2.x;
                obj.angle = Math.abs(Math.atan2(det, dot));
            } else {
                obj.angle = Double.NaN;
            }

            prev2 = prev1;
            prev1 = obj;
            ++i;
        }

        /* speed and aim stars */

        DiffValues aimvals = calc_individual(DIFF_AIM);
        aim = aimvals.difficulty;
        aim_difficulty = aimvals.total;
        aim_length_bonus = length_bonus(aim, aim_difficulty);

        DiffValues speedvals = calc_individual(DIFF_SPEED);
        speed = speedvals.difficulty;
        speed_difficulty = speedvals.total;
        speed_length_bonus = length_bonus(speed, speed_difficulty);

        aim = Math.sqrt(aim) * STAR_SCALING_FACTOR;
        speed = Math.sqrt(speed) * STAR_SCALING_FACTOR;
        if ((mods & MODS_TOUCH_DEVICE) != 0) {
            aim = Math.pow(aim, 0.8);
        }

        /* total stars */
        total = aim + speed +
            Math.abs(speed - aim) * EXTREME_SCALING_FACTOR;

        /* singletap stats */
        for (i = 1; i < beatmap.objects.size(); ++i)
        {
            HitObject prev = beatmap.objects.get(i - 1);
            HitObject obj = beatmap.objects.get(i);

            if (obj.is_single) {
                ++nsingles;
            }

            if ((obj.type & (OBJ_CIRCLE | OBJ_SLIDER)) == 0) {
                continue;
            }

            double interval = (obj.time - prev.time) / speed_mul;

            if (interval >= singletap_threshold) {
                ++nsingles_threshold;
            }
        }

        return this;
    }

    /**
    * @return calc(mods, DEFAULT_SINGLETAP_THRESHOLD)
    * @see DiffCalc#calc(int, double)
    * @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD
    */
    public DiffCalc calc(int mods) {
        return calc(mods, DEFAULT_SINGLETAP_THRESHOLD);
    }

    /**
    * @return calc(MODS_NOMOD, DEFAULT_SINGLETAP_THRESHOLD)
    * @see DiffCalc#calc(int, double)
    * @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD
    */
    public DiffCalc calc() {
        return calc(MODS_NOMOD, DEFAULT_SINGLETAP_THRESHOLD);
    }

    /**
    * sets beatmap field and calls
    * calc(mods, singletap_threshold).
    * @see DiffCalc#calc(int, double)
    */
    public DiffCalc calc(Map beatmap, int mods,
        double singletap_threshold)
    {
        this.beatmap = beatmap;
        return calc(mods, singletap_threshold);
    }

    /**
    * sets beatmap field and calls
    * calc(mods, DEFAULT_SINGLETAP_THRESHOLD).
    * @see DiffCalc#calc(int, double)
    * @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD
    */
    public DiffCalc calc(Map beatmap, int mods) {
        return calc(beatmap, mods, DEFAULT_SINGLETAP_THRESHOLD);
    }

    /**
    * sets beatmap field and calls
    * calc(MODS_NOMOD, DEFAULT_SINGLETAP_THRESHOLD).
    * @see DiffCalc#calc(int, double)
    * @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD
    */
    public DiffCalc calc(Map beatmap)
    {
        return calc(beatmap, MODS_NOMOD,
            DEFAULT_SINGLETAP_THRESHOLD);
    }
}

/* ------------------------------------------------------------- */
/* acc calc                                                      */

public static class Accuracy
{
    public int n300 = 0, n100 = 0, n50 = 0, nmisses = 0;

    public Accuracy() {}

    /**
    * @param n300 the number of 300s, if -1 it will be calculated
    *             from the object count in Accuracy#value(int).
    */
    public Accuracy(int n300, int n100, int n50, int nmisses)
    {
        this.n300 = n300;
        this.n100 = n100;
        this.n50 = n50;
        this.nmisses = nmisses;
    }

    /**
    * calls Accuracy(-1, n100, n50, nmisses) .
    * @see Accuracy#Accuracy(int, int, int, int)
    */
    public Accuracy(int n100, int n50, int nmisses) {
        this(-1, n100, n50, nmisses);
    }

    /**
    * calls Accuracy(-1, n100, n50, 0) .
    * @see Accuracy#Accuracy(int, int, int, int)
    */
    public Accuracy(int n100, int n50) {
        this(-1, n100, n50, 0);
    }

    /**
    * calls Accuracy(-1, n100, 0, 0) .
    * @see Accuracy#Accuracy(int, int, int, int)
    */
    public Accuracy(int n100) {
        this(-1, n100, 0, 0);
    }

    /**
    * rounds to the closest amount of 300s, 100s, 50s for a given
    * accuracy percentage.
    * @param nobjects the total number of hits (n300 + n100 + n50 +
    *        nmisses)
    */
    public Accuracy(double acc_percent, int nmisses, Map beatmap)
    {
    	int nobjects = beatmap.objects.size();
        nmisses = Math.min(nobjects, nmisses);
        int max300 = nobjects - nmisses;

        double maxacc =
            new Accuracy(max300, 0, 0, nmisses).value(beatmap.mode) * 100.0;

        acc_percent = Math.max(0.0, Math.min(maxacc, acc_percent));

        /*
         * CUSTOM: new logic below for computing n100 and n50 since vanilla koohii is kinda inaccurate
         * also changed params for cross mode acc derivation
         */
        
        switch(beatmap.mode) {
        case 0:  //osu
        	n100 = (int) Math.min(max300,  //guard against invalid n100s
                    Math.ceil(   //instead of round to get highest n100
                        -3.0 *
                        ((acc_percent * 0.01 - 1.0) * nobjects + nmisses) *
                        0.5
                    ));
        	//solve x (y = (x*100 + (n - x - m)*300)/(n*300)) where y = acc, n = nobjects, x = n100, m = nmisses
            
            //then use n50 to fill in the gaps; need round
            n50 = (int) Math.round(-2/5.0 * (3 * nmisses + 3 * nobjects * (acc_percent * 0.01 - 1) + 2 * n100));
            //solve z (y = (z * 50 + x*100 + (n - z - x - m)*300)/(n*300)) where everything is same as above and z = n50
            
            //use lowest discrepancy with n100-- until no more or until < epsilon = 0.002%
            double lowestDiscrepancy = 1;
            int n50_ = n50, n100_ = n100;
            while((lowestDiscrepancy > 0.00002 || (n100_ + n50_) > max300) && n100_ > 0) {
            	//get current discrepancy
            	double discrepancy = Math.abs(((acc_percent/100.0) - (n50_*50.0 + n100_*100.0 + (nobjects - n100_ - n50_ - nmisses)*300.0)/(nobjects*300.0)));

            	//only update if better and valid
            	if(discrepancy < lowestDiscrepancy && (n100_ + n50_) <= max300 && n50_ >= 0) {
            		lowestDiscrepancy = discrepancy;
            		n50 = n50_; n100 = n100_;
            	}
            	
            	//tick n100 down by 1 and calc new n50
            	n50_ = (int) Math.round(-2/5.0 * (3 * nmisses + 3 * nobjects * (acc_percent * 0.01 - 1) + 2 * --n100_));
            }
            
            n300 = nobjects - n100 - n50 - nmisses;
            break;
        case 1: //taiko
    		n50 = 0;  //taiko has no n50
    		n100 = (int) Math.min(max300, //guard just like above
    				Math.round((1 - ((double) nmisses)/nobjects - acc_percent/100) * 2 * nobjects));
    		
    		n300 = beatmap.ncircles - n100 - nmisses;  //taiko only cares about circles
    		break;
    	default:
    		throw new IllegalArgumentException("This gamemode is not yet supported.");
        }
        
        if(n300 < 0)
        	throw new IllegalArgumentException("Impossible accuracy inputted. Did you forget to input misses?");
    }

    /**
    * @param nobjects the total number of hits (n300 + n100 + n50 +
    *                 nmiss). if -1, n300 must have been set and
    *                 will be used to deduce this value.
    * @return the accuracy value (0.0-1.0)
    */
    public double value(int nobjects, int mode)
    {
        if (nobjects < 0 && n300 < 0)
        {
            throw new IllegalArgumentException(
                "either nobjects or n300 must be specified"
            );
        }

        int n300_ = n300 >= 0 ? n300 :   //CUSTOM: the guard is <, yet this checks > which makes no sense; changed to >=
            nobjects - n100 - n50 - nmisses;

        if (nobjects < 0) {
            nobjects = n300_ + n100 + n50 + nmisses;
        }

        /*
         * CUSTOM: new logic below for cross mode acc derivation with mode param
         */
        
        double res;
        switch(mode) {
        case 0:  //osu
        	res = (n50 * 50.0 + n100 * 100.0 + n300_ * 300.0) / (nobjects * 300.0);
        	break;
        case 1:  //taiko
            res = (n100 * 0.5 + n300) / (double) (nmisses + n100 + n300_);
        	break;
        default:
        	throw new IllegalArgumentException("This gamemode is not yet supported.");
        }

        return Math.max(0, Math.min(res, 1.0));
    }

    /**
    * calls value(-1) .
    * @see Accuracy#value(int)
    */
    public double value(int mode) {
        return value(-1, mode);
    }
}

/* ------------------------------------------------------------- */
/* pp calc                                                       */

/* base pp value for stars, used internally by ppv2 */
private static
double pp_base(double stars)
{
    return Math.pow(5.0 * Math.max(1.0, stars / 0.0675) - 4.0, 3.0)
        / 100000.0;
}

/**
* parameters to be passed to PPv2.
* aim_stars, speed_stars, max_combo, nsliders, ncircles, nobjects,
* base_ar, base_od are required.
* @see PPv2#PPv2(Koohii.PlayParameters)
* 
* CUSTOM: no longer just used by PPv2, renamed to playparameters instead - it records the play info for pp calculation
*/
public static class PlayParameters
{
    /**
    * if not null, max_combo, nsliders, ncircles, nobjects,
    * base_ar, base_od will be obtained from this beatmap.
    */
    public Map beatmap = null;

    public double aim_stars = 0.0;
    public double speed_stars = 0.0;
    public int max_combo = 0;
    public int nsliders = 0, ncircles = 0, nobjects = 0;

    /** the base AR (before applying mods). */
    public float base_ar = 5.0f;

    /** the base OD (before applying mods). */
    public float base_od = 5.0f;

    /** gamemode. */
    public int mode = MODE_STD;

    /** the mods bitmask, same as osu! api, see MODS_* constants */
    public int mods = MODS_NOMOD;

    /**
    * the maximum combo achieved, if -1 it will default to
    * max_combo - nmiss .
    */
    public int combo = -1;

    /**
    * number of 300s, if -1 it will default to
    * nobjects - n100 - n50 - nmiss .
    */
    public int n300 = -1;
    public int n100 = 0, n50 = 0, nmiss = 0;

    /** scorev1 (1) or scorev2 (2). */
    public int score_version = 1;
}

public static class PPv2
{
    public double total, aim, speed, acc;
    public Accuracy computed_accuracy;

    /**
    * calculates ppv2, results are stored in total, aim, speed,
    * acc, acc_percent.
    * @see PlayParameters
    */
    private PPv2(double aim_stars, double speed_stars,
        int max_combo, int nsliders, int ncircles, int nobjects,
        float base_ar, float base_od, int mode, int mods,
        int combo, int n300, int n100, int n50, int nmiss,
        int score_version, Map beatmap)
    {
        if (beatmap != null)
        {
            mode = beatmap.mode;
            base_ar = beatmap.ar;
            base_od = beatmap.od;
            max_combo = beatmap.max_combo();
            nsliders = beatmap.nsliders;
            ncircles = beatmap.ncircles;
            nobjects = beatmap.objects.size();
        }

        if (mode != MODE_STD)
        {
            throw new UnsupportedOperationException(
                "this gamemode is not yet supported"
            );
        }

        if (max_combo <= 0)
        {
            info("W: max_combo <= 0, changing to 1\n");
            max_combo = 1;
        }

        if (combo < 0) {
            combo = max_combo - nmiss;
        }

        if (n300 < 0) {
            n300 = nobjects - n100 - n50 - nmiss;
        }

        /* accuracy -------------------------------------------- */
        computed_accuracy = new Accuracy(n300, n100, n50, nmiss);
        double accuracy = computed_accuracy.value(0);
        double real_acc = accuracy;

        /* scorev1 ignores sliders since they are free 300s
        and for some reason also ignores spinners */
        int nspinners = nobjects - nsliders - ncircles;

        switch (score_version)
        {
        case 1:
            real_acc = new Accuracy(Math.max(0, n300 - nsliders - nspinners),  //CUSTOM: guard against going below zero
                n100, n50, nmiss).value(0);

            real_acc = Math.max(0.0, real_acc);
            break;

        case 2:
            ncircles = nobjects;
            break;

        default:
            throw new UnsupportedOperationException(
                String.format("unsupported scorev%d",
                    score_version)
            );
        }

        /* global values --------------------------------------- */
        double nobjects_over_2k = nobjects / 2000.0;

        double length_bonus = 0.95 + 0.4 *
            Math.min(1.0, nobjects_over_2k);

        if (nobjects > 2000) {
            length_bonus += Math.log10(nobjects_over_2k) * 0.5;
        }

        double miss_penality_aim =
            0.97 * Math.pow(1 - Math.pow((double)nmiss / nobjects, 0.775), nmiss);
        double miss_penality_speed =
            0.97 * Math.pow(1 - Math.pow((double)nmiss / nobjects, 0.775), Math.pow(nmiss, 0.875));
        double combo_break = Math.pow(combo, 0.8) /
            Math.pow(max_combo, 0.8);

        /* calculate stats with mods */
        MapStats mapstats = new MapStats();
        mapstats.ar = base_ar;
        mapstats.od = base_od;
        mods_apply(mods, mapstats, APPLY_AR | APPLY_OD);

        /* ar bonus -------------------------------------------- */
        double ar_bonus = 0.0;

        if (mapstats.ar > 10.33) {
            ar_bonus += 0.4 * (mapstats.ar - 10.33);
        }

        else if (mapstats.ar < 8.0) {
            ar_bonus +=  0.01 * (8.0 - mapstats.ar);
        }

        /* aim pp ---------------------------------------------- */
        aim = pp_base(aim_stars);
        aim *= length_bonus;
        if (nmiss > 0) {
            aim *= miss_penality_aim;
        }
        aim *= combo_break;
        aim *= 1.0 + Math.min(ar_bonus, ar_bonus * (nobjects / 1000.0));

        double hd_bonus = 1.0;
        if ((mods & MODS_HD) != 0) {
            hd_bonus *= 1.0 + 0.04 * (12.0 - mapstats.ar);
        }
        aim *= hd_bonus;

        if ((mods & MODS_FL) != 0) {
            double fl_bonus = 1.0 + 0.35 * Math.min(1.0, nobjects / 200.0);
            if (nobjects > 200) {
                fl_bonus += 0.3 * Math.min(1.0, (nobjects - 200) / 300.0);
            }
            if (nobjects > 500) {
                fl_bonus += (nobjects - 500) / 1200.0;
            }
            aim *= fl_bonus;
        }

        double acc_bonus = 0.5 + accuracy / 2.0;
        double od_squared = mapstats.od * mapstats.od;
        double od_bonus = 0.98 + od_squared / 2500.0;

        aim *= acc_bonus;
        aim *= od_bonus;

        /* speed pp -------------------------------------------- */
        speed = pp_base(speed_stars);
        speed *= length_bonus;
        if (nmiss > 0) {
            speed *= miss_penality_speed;
        }
        speed *= combo_break;
        if (mapstats.ar > 10.33) {
            speed *= 1.0 + Math.min(ar_bonus, ar_bonus * (nobjects / 1000.0));
        }
        speed *= hd_bonus;

        /* similar to aim acc and od bonus */
        speed *= (0.95 + od_squared / 750.0) *
            Math.pow(accuracy, (14.5 - Math.max(mapstats.od, 8)) / 2);
        speed *= Math.pow(0.98, n50 < nobjects / 500.0 ? 0.00 : n50 - nobjects / 500.0);

        /* acc pp ---------------------------------------------- */
        acc = Math.pow(1.52163, mapstats.od) *
            Math.pow(real_acc, 24.0) * 2.83;

        acc *= Math.min(1.15, Math.pow(ncircles / 1000.0, 0.3));

        if ((mods & MODS_HD) != 0) {
            acc *= 1.08;
        }

        if ((mods & MODS_FL) != 0) {
            acc *= 1.02;
        }

        /* total pp -------------------------------------------- */
        double final_multiplier = 1.12;

        if ((mods & MODS_NF) != 0) {
            final_multiplier *= Math.max(0.9, 1.0 - 0.2 * nmiss);
        }

        if ((mods & MODS_SO) != 0) {
            final_multiplier *= 1.0 - Math.pow((double)nspinners / nobjects, 0.85);
        }

        total = Math.pow(
            Math.pow(aim, 1.1) + Math.pow(speed, 1.1) +
            Math.pow(acc, 1.1),
            1.0 / 1.1
        ) * final_multiplier;
    }

    /** @see PlayParameters */
    public PPv2(PlayParameters p)
    {
        this(p.aim_stars, p.speed_stars, p.max_combo, p.nsliders,
            p.ncircles, p.nobjects, p.base_ar, p.base_od, p.mode,
            p.mods, p.combo, p.n300, p.n100, p.n50, p.nmiss,
            p.score_version, p.beatmap);
    }

    /**
    * simplest possible call, calculates ppv2 for SS scorev1.
    * @see PPv2#PPv2(Koohii.PlayParameters)
    */
    public PPv2(double aim_stars, double speed_stars, Map b)
    {
        this(aim_stars, speed_stars, -1, b.nsliders, b.ncircles,
            b.objects.size(), b.ar, b.od, b.mode, MODS_NOMOD, -1,
            -1, 0, 0, 0, 1, b);
    }
}

/**
 * CUSTOM: Taiko support added by me
 * Heavily modified from https://mon.im/taikopp/ to fit Koohii
 */

public static class TaikoPP {
	
	public Map map;
	public double stars;
	public int totalHits;
	public int misses;
	public int combo;
	public int mods;
	public double acc;
	public double computedAcc;
	public double rawod;
	public double strain;
	public double pp;
	
	//technically taiko dont have aim stars and the only other star val is speed stars, so imma just borrow that variable
	public TaikoPP(PlayParameters p) {
		this(p.speed_stars, p.combo, p.mods, new Accuracy(p.n300, p.n100, p.n50, p.nmiss).value(1), p.nmiss, p.beatmap);
	}
	
	public TaikoPP(double stars, int combo, int mods, double acc, int misses, Map beatmap) {
		this.map = beatmap;
		this.stars = stars;
		this.totalHits = beatmap.ncircles;
		this.misses = misses;
		this.combo = combo;
		this.mods = mods;
		this.acc = acc;
		this.rawod = beatmap.od;
		if(this.acc > 1) {
			this.acc /= 100;
		}
		this.strain = computeStrain(this.combo, this.misses, this.acc);
		this.computedAcc = computeAcc(this.acc);
		this.pp = computeTotal(this.strain, this.computedAcc);
	}
	
	private double computeTotal(double strain, double acc) {
		double multiplier = 1.1;
		if ((mods & MODS_NF) != 0) multiplier *= 0.9;
		if ((mods & MODS_HD) != 0) multiplier *= 1.1;
		return Math.pow(Math.pow(strain, 1.1) + Math.pow(acc, 1.1), 1.0 / 1.1) * multiplier;
	}
	
	private double computeStrain(int combo, int misses, double acc) {
		double strainValue = Math.pow(5 * Math.max(1, this.stars / 0.0075) - 4, 2) / 100000;
		double lengthBonus = 1 + 0.1 * Math.min(1, (this.totalHits / 1500.0));
		strainValue *= lengthBonus * Math.pow(0.985, misses);
		
		//if(combo > 0) strainValue *= Math.min(Math.pow(this.totalHits, 0.5) / Math.pow(combo, 0.5), 1);
		if ((mods & MODS_HD) != 0) strainValue *= 1.025;
		if ((mods & MODS_FL) != 0) strainValue *= (1.05 * lengthBonus);
		
		strainValue *= acc;
		return strainValue;
	}
	
	private double computeAcc(double acc) {
		double hitWindow300 = hitWindow();
		if(hitWindow300 <= 0) return -1;
		return Math.pow(150 / hitWindow300, 1.1) * Math.pow(acc, 15) * 22 * Math.min(Math.pow(this.totalHits / 1500.0, 0.3), 1.15);
	}
	
	private double hitWindow() {
		double od = scaleOD();
		double max = 20, min = 50;
		double result = Math.floor(min + (max - min) * od / 10) - 0.5;
		if ((mods & MODS_HT) != 0) result /= 0.75;
		if ((mods & (MODS_DT | MODS_NC)) != 0) result /= 1.5;
		return Math.round(result * 100) / 100.0;
	}
	
	private double scaleOD() {
		double od = this.rawod;
		if ((mods & MODS_EZ) != 0) od /= 2;
		if ((mods & MODS_HR) != 0) od *= 1.4;
		od = Math.max(Math.min(od, 10), 0);
		return od;
	}
	
	//just realized how weird this method was implemented with how it modifies stats similar to pass by ref and still returns that object
	//but hey imma stick with it for consistency's sake
	public MapStats taiko_mods_apply(int mods, MapStats stats) {
		stats.od = (float) scaleOD();
		mods_apply(mods, stats, APPLY_HP);  //hp scaling works the same in taiko, just reuse
		return stats;
	}
	
}

} /* public final class Koohii */
