/*
 * SPDX-License-Identifier: MIT
 */
package org.ta4j.core.indicators.candles;

import java.util.Objects;

import org.ta4j.core.Bar;
import org.ta4j.core.BarSeries;
import org.ta4j.core.indicators.CachedIndicator;
import org.ta4j.core.indicators.trend.DownTrendIndicator;
import org.ta4j.core.num.Num;

/**
 * Piercing line candlestick pattern indicator.
 *
 * <p>
 * A piercing line pattern is detected when a long bearish candle in a downtrend
 * is followed by a long bullish candle that gaps down at the open and closes
 * deep into the first candle's body.
 * </p>
 *
 * <p>
 * Invalid ratio denominators (zero or NaN in first open, second open, or first
 * close) are treated as non-pattern bars and return {@code false}.
 * </p>
 *
 * <p>
 * Default thresholds:
 * </p>
 * <ul>
 * <li>{@code bigBodyThresholdPercentage = 0.03} (3%)</li>
 * <li>{@code gapThresholdPercentage = 0.0} (any strict gap down)</li>
 * <li>{@code penetrationThresholdPercentage = 0.5} (close above midpoint)</li>
 * </ul>
 *
 * @see <a href=
 *      "https://www.investopedia.com/terms/p/piercingpattern.asp">Piercing
 *      Pattern</a>
 * @since 0.22.3
 */
public class PiercingLineIndicator extends CachedIndicator<Boolean> {

    private final DownTrendIndicator trendIndicator;
    private final RealBodyIndicator realBodyIndicator;
    private final Num bigBodyThresholdPercentage;
    private final Num gapThresholdPercentage;
    private final Num penetrationThresholdPercentage;

    /**
     * Constructor using default thresholds.
     *
     * @param series the bar series
     */
    public PiercingLineIndicator(final BarSeries series) {
        this(series, series.numFactory().numOf(0.03), series.numFactory().zero(), series.numFactory().numOf(0.5));
    }

    /**
     * Constructor exposing all tunable thresholds.
     *
     * @param series                         the bar series
     * @param bigBodyThresholdPercentage     minimum body size ratio (body/open)
     * @param gapThresholdPercentage         minimum gap down ratio from first close
     *                                       to second open
     * @param penetrationThresholdPercentage minimum penetration ratio into the
     *                                       first body measured from first close
     *                                       upward
     */
    public PiercingLineIndicator(final BarSeries series, final Num bigBodyThresholdPercentage,
            final Num gapThresholdPercentage, final Num penetrationThresholdPercentage) {
        super(Objects.requireNonNull(series, "series must not be null"));
        this.trendIndicator = new DownTrendIndicator(getBarSeries());
        this.realBodyIndicator = new RealBodyIndicator(getBarSeries());
        this.bigBodyThresholdPercentage = Objects.requireNonNull(bigBodyThresholdPercentage,
                "bigBodyThresholdPercentage must not be null");
        this.gapThresholdPercentage = Objects.requireNonNull(gapThresholdPercentage,
                "gapThresholdPercentage must not be null");
        this.penetrationThresholdPercentage = Objects.requireNonNull(penetrationThresholdPercentage,
                "penetrationThresholdPercentage must not be null");
    }

    @Override
    protected Boolean calculate(final int index) {
        if (index < getCountOfUnstableBars()) {
            return false;
        }

        final Bar firstBar = getBarSeries().getBar(index - 1);
        final Bar secondBar = getBarSeries().getBar(index);
        final Num firstOpenPrice = firstBar.getOpenPrice();
        final Num secondOpenPrice = secondBar.getOpenPrice();
        final Num firstClosePrice = firstBar.getClosePrice();
        if (isInvalidDenominator(firstOpenPrice) || isInvalidDenominator(secondOpenPrice)
                || isInvalidDenominator(firstClosePrice)) {
            return false;
        }

        final Num firstBodyRatio = realBodyIndicator.getValue(index - 1).abs().dividedBy(firstOpenPrice);
        final Num secondBodyRatio = realBodyIndicator.getValue(index).abs().dividedBy(secondOpenPrice);

        final Num firstBodySize = firstOpenPrice.minus(firstClosePrice);
        final Num requiredClose = firstClosePrice.plus(firstBodySize.multipliedBy(penetrationThresholdPercentage));
        final Num gapRatio = firstClosePrice.minus(secondOpenPrice).dividedBy(firstClosePrice);
        if (isInvalidValue(firstBodyRatio) || isInvalidValue(secondBodyRatio) || isInvalidValue(requiredClose)
                || isInvalidValue(gapRatio)) {
            return false;
        }

        return firstBar.isBearish() && firstBodyRatio.isGreaterThanOrEqual(bigBodyThresholdPercentage)
                && secondBar.isBullish() && secondBodyRatio.isGreaterThanOrEqual(bigBodyThresholdPercentage)
                && secondOpenPrice.isLessThan(firstClosePrice) && gapRatio.isGreaterThanOrEqual(gapThresholdPercentage)
                && secondBar.getClosePrice().isGreaterThan(requiredClose)
                && secondBar.getClosePrice().isLessThan(firstOpenPrice) && trendIndicator.getValue(index);
    }

    private static boolean isInvalidDenominator(final Num value) {
        return isInvalidValue(value) || value.isZero();
    }

    private static boolean isInvalidValue(final Num value) {
        return Num.isNaNOrNull(value) || Double.isNaN(value.doubleValue());
    }

    @Override
    public int getCountOfUnstableBars() {
        return Math.max(1, trendIndicator.getCountOfUnstableBars());
    }
}
