/*
 * Copyright 2008 ZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use crate::{
    Exceptions, Point,
    common::{
        BitMatrix, DefaultGridSampler, GridSampler, Quadrilateral, Result,
        detector::WhiteRectangleDetector,
    },
    point,
};

use super::DatamatrixDetectorResult;

/**
 * <p>Encapsulates logic that can detect a Data Matrix Code in an image, even if the Data Matrix Code
 * is rotated or skewed, or partially obscured.</p>
 *
 * @author Sean Owen
 */
pub struct Detector<'a> {
    image: &'a BitMatrix,
    rectangleDetector: WhiteRectangleDetector<'a>,
}
impl<'a> Detector<'_> {
    pub fn new(image: &'a BitMatrix) -> Result<Detector<'a>> {
        Ok(Detector {
            rectangleDetector: WhiteRectangleDetector::new_from_image(image)?,
            image,
        })
    }

    /**
     * <p>Detects a Data Matrix Code in an image.</p>
     *
     * @return {@link DetectorRXingResult} encapsulating results of detecting a Data Matrix Code
     * @throws NotFoundException if no Data Matrix Code can be found
     */
    pub fn detect(&self) -> Result<DatamatrixDetectorResult> {
        let cornerPoints = self.rectangleDetector.detect()?;

        let mut points = self.detectSolid1(cornerPoints);
        points = self.detectSolid2(points);
        if let Some(point) = self.correctTopRight(&points) {
            points[3] = point;
        } else {
            return Err(Exceptions::not_found_with("point 4 unfound"));
        }
        // points[3] = self.correctTopRight(&points);
        // if points[3] == null {
        //   throw NotFoundException.getNotFoundInstance();
        // }
        points = self.shiftToModuleCenter(points);

        let topLeft = points[0];
        let bottomLeft = points[1];
        let bottomRight = points[2];
        let topRight = points[3];

        let mut dimensionTop = self.transitionsBetween(topLeft, topRight) + 1;
        let mut dimensionRight = self.transitionsBetween(bottomRight, topRight) + 1;
        if (dimensionTop & 0x01) == 1 {
            dimensionTop += 1;
        }
        if (dimensionRight & 0x01) == 1 {
            dimensionRight += 1;
        }

        if 4 * dimensionTop < 6 * dimensionRight && 4 * dimensionRight < 6 * dimensionTop {
            // The matrix is square
            dimensionTop = dimensionTop.max(dimensionRight);
            dimensionRight = dimensionTop.max(dimensionRight);
        }

        let bits = Self::sampleGrid(
            self.image,
            topLeft,
            bottomLeft,
            bottomRight,
            topRight,
            dimensionTop,
            dimensionRight,
        )?;

        Ok(DatamatrixDetectorResult::new(
            bits,
            vec![topLeft, bottomLeft, bottomRight, topRight],
        ))
    }

    #[inline]
    fn shiftPoint(p: Point, to: Point, div: u32) -> Point {
        let x = (to.x - p.x) / (div as f32 + 1.0);
        let y = (to.y - p.y) / (div as f32 + 1.0);
        point(p.x + x, p.y + y)
    }

    #[inline]
    fn moveAway(p: Point, fromX: f32, fromY: f32) -> Point {
        let mut x = p.x;
        let mut y = p.y;

        if x < fromX {
            x -= 1.0;
        } else {
            x += 1.0;
        }

        if y < fromY {
            y -= 1.0;
        } else {
            y += 1.0;
        }

        point(x, y)
    }

    /**
     * Detect a solid side which has minimum transition.
     */
    fn detectSolid1(&self, cornerPoints: [Point; 4]) -> [Point; 4] {
        // 0  2
        // 1  3
        let pointA = cornerPoints[0];
        let pointB = cornerPoints[1];
        let pointC = cornerPoints[3];
        let pointD = cornerPoints[2];

        let trAB = self.transitionsBetween(pointA, pointB);
        let trBC = self.transitionsBetween(pointB, pointC);
        let trCD = self.transitionsBetween(pointC, pointD);
        let trDA = self.transitionsBetween(pointD, pointA);

        // 0..3
        // :  :
        // 1--2
        let mut min = trAB;
        let mut points = [pointD, pointA, pointB, pointC];
        if min > trBC {
            min = trBC;
            points[0] = pointA;
            points[1] = pointB;
            points[2] = pointC;
            points[3] = pointD;
        }
        if min > trCD {
            min = trCD;
            points[0] = pointB;
            points[1] = pointC;
            points[2] = pointD;
            points[3] = pointA;
        }
        if min > trDA {
            points[0] = pointC;
            points[1] = pointD;
            points[2] = pointA;
            points[3] = pointB;
        }

        points
    }

    /**
     * Detect a second solid side next to first solid side.
     */
    fn detectSolid2(&self, points: [Point; 4]) -> [Point; 4] {
        // A..D
        // :  :
        // B--C
        let [pointA, pointB, pointC, pointD] = points;

        // Transition detection on the edge is not stable.
        // To safely detect, shift the points to the module center.
        let tr = self.transitionsBetween(pointA, pointD);
        let pointBs = Self::shiftPoint(pointB, pointC, (tr + 1) * 4);
        let pointCs = Self::shiftPoint(pointC, pointB, (tr + 1) * 4);
        let trBA = self.transitionsBetween(pointBs, pointA);
        let trCD = self.transitionsBetween(pointCs, pointD);

        // 0..3
        // |  :
        // 1--2
        if trBA < trCD {
            // solid sides: A-B-C
            [pointA, pointB, pointC, pointD]
            // points[0] = pointA;
            // points[1] = pointB;
            // points[2] = pointC;
            // points[3] = pointD;
        } else {
            // solid sides: B-C-D
            [pointB, pointC, pointD, pointA]
            // points[0] = pointB;
            // points[1] = pointC;
            // points[2] = pointD;
            // points[3] = pointA;
        }
    }

    /**
     * Calculates the corner position of the white top right module.
     */
    fn correctTopRight(&self, points: &[Point; 4]) -> Option<Point> {
        // A..D
        // |  :
        // B--C
        let pointA = points[0];
        let pointB = points[1];
        let pointC = points[2];
        let pointD = points[3];

        // shift points for safe transition detection.
        let mut trTop = self.transitionsBetween(pointA, pointD);
        let mut trRight = self.transitionsBetween(pointB, pointD);
        let pointAs = Self::shiftPoint(pointA, pointB, (trRight + 1) * 4);
        let pointCs = Self::shiftPoint(pointC, pointB, (trTop + 1) * 4);

        trTop = self.transitionsBetween(pointAs, pointD);
        trRight = self.transitionsBetween(pointCs, pointD);

        let candidate1 = point(
            pointD.x + (pointC.x - pointB.x) / (trTop as f32 + 1.0),
            pointD.y + (pointC.y - pointB.y) / (trTop as f32 + 1.0),
        );
        let candidate2 = point(
            pointD.x + (pointA.x - pointB.x) / (trRight as f32 + 1.0),
            pointD.y + (pointA.y - pointB.y) / (trRight as f32 + 1.0),
        );

        if !self.isValid(candidate1) {
            if self.isValid(candidate2) {
                return Some(candidate2);
            }
            return None;
        }
        if !self.isValid(candidate2) {
            return Some(candidate1);
        }

        let sumc1 = self.transitionsBetween(pointAs, candidate1)
            + self.transitionsBetween(pointCs, candidate1);
        let sumc2 = self.transitionsBetween(pointAs, candidate2)
            + self.transitionsBetween(pointCs, candidate2);

        if sumc1 > sumc2 {
            Some(candidate1)
        } else {
            Some(candidate2)
        }
    }

    /**
     * Shift the edge points to the module center.
     */
    fn shiftToModuleCenter(&self, points: [Point; 4]) -> [Point; 4] {
        // A..D
        // |  :
        // B--C
        let [mut pointA, mut pointB, mut pointC, mut pointD] = points;

        // calculate pseudo dimensions
        let mut dimH = self.transitionsBetween(pointA, pointD) + 1;
        let mut dimV = self.transitionsBetween(pointC, pointD) + 1;

        // shift points for safe dimension detection
        let mut pointAs = Self::shiftPoint(pointA, pointB, dimV * 4);
        let mut pointCs = Self::shiftPoint(pointC, pointB, dimH * 4);

        //  calculate more precise dimensions
        dimH = self.transitionsBetween(pointAs, pointD) + 1;
        dimV = self.transitionsBetween(pointCs, pointD) + 1;
        if (dimH & 0x01) == 1 {
            dimH += 1;
        }
        if (dimV & 0x01) == 1 {
            dimV += 1;
        }

        // WhiteRectangleDetector returns points inside of the rectangle.
        // I want points on the edges.
        let centerX = (pointA.x + pointB.x + pointC.x + pointD.x) / 4.0;
        let centerY = (pointA.y + pointB.y + pointC.y + pointD.y) / 4.0;
        pointA = Self::moveAway(pointA, centerX, centerY);
        pointB = Self::moveAway(pointB, centerX, centerY);
        pointC = Self::moveAway(pointC, centerX, centerY);
        pointD = Self::moveAway(pointD, centerX, centerY);

        let mut pointBs;
        let mut pointDs;

        // shift points to the center of each modules
        pointAs = Self::shiftPoint(pointA, pointB, dimV * 4);
        pointAs = Self::shiftPoint(pointAs, pointD, dimH * 4);
        pointBs = Self::shiftPoint(pointB, pointA, dimV * 4);
        pointBs = Self::shiftPoint(pointBs, pointC, dimH * 4);
        pointCs = Self::shiftPoint(pointC, pointD, dimV * 4);
        pointCs = Self::shiftPoint(pointCs, pointB, dimH * 4);
        pointDs = Self::shiftPoint(pointD, pointC, dimV * 4);
        pointDs = Self::shiftPoint(pointDs, pointA, dimH * 4);

        [pointAs, pointBs, pointCs, pointDs]
    }

    #[inline]
    fn isValid(&self, p: Point) -> bool {
        p.x >= 0.0
            && p.x <= self.image.getWidth() as f32 - 1.0
            && p.y > 0.0
            && p.y <= self.image.getHeight() as f32 - 1.0
    }

    fn sampleGrid(
        image: &BitMatrix,
        topLeft: Point,
        bottomLeft: Point,
        bottomRight: Point,
        topRight: Point,
        dimensionX: u32,
        dimensionY: u32,
    ) -> Result<BitMatrix> {
        let sampler = DefaultGridSampler;

        let dst = Quadrilateral::new(
            point(0.5, 0.5),
            point(dimensionX as f32 - 0.5, 0.5),
            point(dimensionX as f32 - 0.5, dimensionY as f32 - 0.5),
            point(0.5, dimensionY as f32 - 0.5),
        );
        let src = Quadrilateral::new(topRight, topLeft, bottomRight, bottomLeft);

        let (res, _) = sampler.sample_grid_detailed(image, dimensionX, dimensionY, dst, src)?;
        Ok(res)
    }

    /**
     * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm.
     */
    fn transitionsBetween(&self, from: Point, to: Point) -> u32 {
        // See QR Code Detector, sizeOfBlackWhiteBlackRun()
        let mut fromX = from.x.floor() as i32;
        let mut fromY = from.y.floor() as i32;
        let mut toX = to.x.floor() as i32;
        let mut toY = (self.image.getHeight() - 1).min(to.y.floor() as u32) as i32;

        let steep = (toY - fromY).abs() > (toX - fromX).abs();
        if steep {
            std::mem::swap(&mut fromX, &mut fromY);
            std::mem::swap(&mut toX, &mut toY);
        }

        let dx = (toX - fromX).abs();
        let dy = (toY - fromY).abs();
        let mut error = -dx / 2;
        let ystep = if fromY < toY { 1 } else { -1 };
        let xstep = if fromX < toX { 1 } else { -1 };
        let mut transitions = 0;
        let mut inBlack = self.image.get(
            if steep { fromY as u32 } else { fromX as u32 },
            if steep { fromX as u32 } else { fromY as u32 },
        );
        let mut x = fromX;
        let mut y = fromY;
        while x != toX {
            // for (int x = fromX, y = fromY; x != toX; x += xstep) {
            let isBlack = self.image.get(
                if steep { y as u32 } else { x as u32 },
                if steep { x as u32 } else { y as u32 },
            );
            if isBlack != inBlack {
                transitions += 1;
                inBlack = isBlack;
            }
            error += dy;
            if error > 0 {
                if y == toY {
                    break;
                }
                y += ystep;
                error -= dx;
            }

            x += xstep;
        }
        transitions
    }
}
