// Copyright 2024 The Grim Developers
//
// 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 std::sync::Arc;
use parking_lot::RwLock;
use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, RichText, TextureOptions, Widget};
use image::{DynamicImage, EncodableLayout, ImageFormat};

use grin_util::ZeroingString;
use grin_wallet_libwallet::SlatepackAddress;
use grin_keychain::mnemonic::WORDS;

use crate::gui::Colors;
use crate::gui::icons::CAMERA_ROTATE;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{QrScanResult, QrScanState};
use crate::gui::views::View;
use crate::wallet::types::PhraseSize;
use crate::wallet::WalletUtils;

/// Camera QR code scanner.
pub struct CameraContent {
    /// QR code scanning progress and result.
    qr_scan_state: Arc<RwLock<QrScanState>>,

    /// Uniform Resources URIs collected from QR code scanning.
    ur_data: Arc<RwLock<Option<(Vec<String>, usize)>>>,
}

impl Default for CameraContent {
    fn default() -> Self {
        Self {
            qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
            ur_data: Arc::new(RwLock::new(None)),
        }
    }
}

impl CameraContent {
    /// Draw camera content.
    pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
        // Draw last image from camera or loader.
        if let Some(img_data) = cb.camera_image() {
            // Load image to draw.
            if let Ok(mut img) =
                image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
                // Process image to find QR code.
                self.scan_qr(&img);
                // Setup image rotation.
                img = match img_data.1 {
                    90 => img.rotate90(),
                    180 => img.rotate180(),
                    270 => img.rotate270(),
                    _ => img
                };
                // Convert to ColorImage to add at content.
                let color_img = match &img {
                    DynamicImage::ImageRgb8(image) => {
                        egui::ColorImage::from_rgb(
                            [image.width() as usize, image.height() as usize],
                            image.as_bytes(),
                        )
                    },
                    other => {
                        let image = other.to_rgba8();
                        egui::ColorImage::from_rgba_unmultiplied(
                            [image.width() as usize, image.height() as usize],
                            image.as_bytes(),
                        )
                    },
                };
                // Create image texture.
                let texture = ui.ctx().load_texture("camera_image",
                                                    color_img.clone(),
                                                    TextureOptions::default());
                let img_size = egui::emath::vec2(color_img.width() as f32,
                                                 color_img.height() as f32);
                let sized_img = SizedTexture::new(texture.id(), img_size);
                // Add image to content.
                ui.vertical_centered(|ui| {
                    egui::Image::from_texture(sized_img)
                        // Setup to crop image at square.
                        .uv(Rect::from([
                            Pos2::new(1.0 - (img_size.y / img_size.x), 0.0),
                            Pos2::new(1.0, 1.0)
                        ]))
                        .max_height(ui.available_width())
                        .maintain_aspect_ratio(false)
                        .shrink_to_fit()
                        .ui(ui);
                });

                // Show UR scan progress.
                let show_ur_progress = {
                    self.ur_data.clone().read().is_some()
                };
                let ur_progress = self.ur_progress();
                if show_ur_progress && ur_progress != 0 {
                    ui.add_space(-52.0);
                    ui.vertical_centered(|ui| {
                        ui.label(RichText::new(format!("{}%", ur_progress))
                            .size(16.0)
                            .color(Colors::yellow()));
                    });
                }

                // Show button to switch cameras.
                if cb.can_switch_camera() {
                    ui.add_space(-52.0);
                    let mut size = ui.available_size();
                    size.y = 48.0;
                    ui.allocate_ui_with_layout(size, Layout::right_to_left(Align::Max), |ui| {
                        ui.add_space(4.0);
                        View::button(ui, CAMERA_ROTATE.to_string(), Colors::white_or_black(false), || {
                            cb.switch_camera();
                        });
                    });
                }
            } else {
                self.loading_content_ui(ui);
            }
        } else {
            self.loading_content_ui(ui);
        }

        // Request redraw.
        ui.ctx().request_repaint();
    }

    /// Draw camera loading progress content.
    fn loading_content_ui(&self, ui: &mut egui::Ui) {
        let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
        ui.vertical_centered(|ui| {
            ui.add_space(space);
            View::big_loading_spinner(ui);
            ui.add_space(space);
        });
    }

    /// Check if image is processing to find QR code.
    fn image_processing(&self) -> bool {
        let r_scan = self.qr_scan_state.read();
        r_scan.image_processing
    }

    /// Get UR scanning progress in percents.
    fn ur_progress(&self) -> i32 {
        // Setup data.
        let r_data = self.ur_data.read();
        let (data, total) = r_data.clone().unwrap_or((vec![], 0));
        if data.is_empty() {
            return 0;
        }
        // Calculate progress.
        let mut complete = 0;
        for i in &data {
            if !i.is_empty() {
                complete += 1;
            }
        }
        (100 * complete / total) as i32
    }

    /// Parse QR code from provided image data.
    fn scan_qr(&self, image_data: &DynamicImage) {
        // Do not scan when another image is processing.
        if self.image_processing() {
            return;
        }
        // Setup scanning flag.
        {
            let mut w_scan = self.qr_scan_state.write();
            w_scan.image_processing = true;
        }

        let image_data = image_data.clone();
        let qr_scan_state = self.qr_scan_state.clone();
        let ur_data = self.ur_data.clone();

        let on_scan = async move {
            // Prepare image data.
            let img = image_data.to_luma8();
            let mut img: rqrr::PreparedImage<image::GrayImage>
                = rqrr::PreparedImage::prepare(img);
            // Scan and save results.
            let grids = img.detect_grids();
            if let Some(g) = grids.get(0) {
                let mut qr_data = vec![];
                if let Ok(_) = g.decode_to(&mut qr_data) {
                    // Setup scanned data into text.
                    let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
                    // Setup current text.
                    let cur_text = {
                        let r_scan = qr_scan_state.read();
                        let text = if let Some(res) = r_scan.qr_scan_result.clone() {
                            res.text()
                        } else {
                            "".to_string()
                        };
                        text
                    };
                    // Parse non-empty data if parsed text is different from saved.
                    if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
                        let res = Self::parse_qr_code(qr_data);
                        match res {
                            QrScanResult::URPart(uri, index, total) => {
                                // Setup current UR data.
                                let mut cur_data = {
                                    let r_data = ur_data.read();
                                    let mut cur_data = vec!["".to_string(); total];
                                    if let Some((d, _)) = r_data.clone() {
                                        cur_data = d;
                                    }
                                    cur_data
                                };
                                if !cur_data.contains(&uri) {
                                    // Save part of UR data.
                                    {
                                        cur_data.insert(index, uri);
                                        let mut w_data = ur_data.write();
                                        *w_data = Some((cur_data.clone(), total));
                                    }
                                    // Setup UR decoder.
                                    let mut decoder = ur::Decoder::default();
                                    for m in cur_data {
                                        if !m.is_empty() {
                                            if let Ok(_) = decoder.receive(m.as_str()) {
                                                continue;
                                            } else {
                                                break;
                                            }
                                        }
                                    }
                                    // Check if UR data is complete.
                                    if decoder.complete() {
                                        if let Ok(data) = decoder.message() {
                                            // Parse complete data.
                                            let res = Self::parse_qr_code(data.unwrap_or(vec![]));
                                            // Clean UR data.
                                            let mut w_data = ur_data.write();
                                            *w_data = None;
                                            // Save scan result.
                                            let mut w_scan = qr_scan_state.write();
                                            w_scan.qr_scan_result = Some(res);
                                            return;
                                        }
                                    }
                                }
                            }
                            _ => {
                                // Clean UR data.
                                let mut w_data = ur_data.write();
                                *w_data = None;
                                // Save scan result.
                                let mut w_scan = qr_scan_state.write();
                                w_scan.qr_scan_result = Some(res);
                                return;
                            }
                        }
                    }
                }
            }
            // Reset scanning flag to process again.
            {
                let mut w_scan = qr_scan_state.write();
                w_scan.image_processing = false;
            }
        };

        // Launch scanner at separate thread.
        thread::spawn(move || {
            tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .unwrap()
                .block_on(on_scan);
        });
    }

    /// Parse QR code scan result.
    fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
        // Check if string starts with Grin address prefix.
        let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
        let text = text_string.trim();
        if text.starts_with("tgrin") || text.starts_with("grin") {
            if SlatepackAddress::try_from(text).is_ok() {
                return QrScanResult::Address(ZeroingString::from(text));
            }
        }

        // Check if string contains Slatepack message prefix and postfix.
        if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
            return QrScanResult::Slatepack(ZeroingString::from(text));
        }

        // Check Uniform Resource data.
        // https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
        if text.starts_with("ur:bytes/") {
            let split = text.split("/").collect::<Vec<_>>();
            if let Some(index_total) = split.get(1) {
                if let Some((index, total)) = index_total.split_once("-") {
                    let index = index.parse::<usize>();
                    let total = total.parse::<usize>();
                    if index.is_ok() && total.is_ok() {
                        let index = index.unwrap() - 1;
                        let total = total.unwrap();
                        return QrScanResult::URPart(text_string, index, total);
                    }
                }
            }
        }

        // Check Compact SeedQR format.
        // https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
        if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
            // Setup words amount.
            let total_bits = data.len() * 8;
            let checksum_bits = total_bits / 32;
            let total_words = (total_bits + checksum_bits) / 11;
            // Setup entropy.
            let mut entropy = data.clone();
            WalletUtils::setup_checksum(&mut entropy);
            // Setup bits.
            let mut bits = vec![false; entropy.len() * 8];
            for i in 0..entropy.len() {
                for j in 0..8 {
                    bits[(i * 8) + j] = (entropy[i] & (1 << (7 - j))) != 0;
                }
            }
            // Extract word index.
            let extract_index = |i: usize| -> usize {
                let mut index = 0;
                for j in 0..11 {
                    index = index << 1;
                    if bits[(i * 11) + j] {
                        index += 1;
                    }
                }
                return index;
            };
            // Setup words.
            let mut words = "".to_string();
            for n in 0..total_words {
                // Setup word index.
                let index = extract_index(n);
                // Setup word.
                let empty_word = "".to_string();
                let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
                if word.is_empty() {
                    words = empty_word;
                    break;
                }
                words = if words.is_empty() {
                    format!("{}", word)
                } else {
                    format!("{} {}", words, word)
                };
            }
            if !words.is_empty() {
                return QrScanResult::SeedQR(ZeroingString::from(words));
            }
        }

        // Check Standard SeedQR format.
        // https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
        let only_numbers = || {
            for c in text.chars() {
                if !c.is_numeric() {
                    return false;
                }
            }
            true
        };
        if !text.is_empty() && data.len() <= 96 && data.len() % 4 == 0 && only_numbers() {
            if let Some(_) = PhraseSize::type_for_value(text.len() / 4) {
                let chars: Vec<char> = text.trim().chars().collect();
                let split = &chars.chunks(4)
                    .map(|chunk| chunk.iter().collect::<String>()
                        .trim()
                        .trim_start_matches("0")
                        .to_string()
                    )
                    .collect::<Vec<_>>();
                let mut words = "".to_string();
                for i in split {
                    let index = if i.is_empty() {
                        0usize
                    } else {
                        i.parse::<usize>().unwrap_or(WORDS.len())
                    };
                    let empty_word = "".to_string();
                    let word = WORDS.get(index).clone().unwrap_or(&empty_word).clone();
                    // Return text result when BIP39 word was not found.
                    if word.is_empty() {
                        return QrScanResult::Text(ZeroingString::from(text));
                    }
                    words = if words.is_empty() {
                        format!("{}", word)
                    } else {
                        format!("{} {}", words, word)
                    };
                }
                return QrScanResult::SeedQR(ZeroingString::from(words));
            }
        }

        // Return default text result.
        QrScanResult::Text(ZeroingString::from(text))
    }

    /// Get QR code scan result.
    pub fn qr_scan_result(&self) -> Option<QrScanResult> {
        let r_scan = self.qr_scan_state.read();
        if r_scan.qr_scan_result.is_some() {
            return Some(r_scan.qr_scan_result.clone().unwrap());
        }
        None
    }
}