// Copyright 2022 The piet-gpu 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
//
//     https://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.
//
// Also licensed under MIT license, at your choice.

use piet_gpu::{EncodedSceneRef, PixelFormat, RenderConfig};
use piet_gpu_hal::{QueryPool, Session};
use piet_scene::geometry::{Affine, Rect};
use piet_scene::glyph::pinot::{types::Tag, FontDataRef};
use piet_scene::glyph::{GlyphContext, GlyphProvider};
use piet_scene::resource::ResourceContext;
use piet_scene::scene::{Fragment, Scene};

/// State and resources for rendering a scene.
pub struct PgpuRenderer {
    session: Session,
    pgpu_renderer: Option<piet_gpu::Renderer>,
    query_pool: QueryPool,
    width: u32,
    height: u32,
    is_color: bool,
}

impl PgpuRenderer {
    #[cfg(all(
        not(target_arch = "wasm32"),
        any(target_os = "ios", target_os = "macos")
    ))]
    pub fn new(device: &metal::DeviceRef, queue: &metal::CommandQueueRef) -> Self {
        let piet_device = piet_gpu_hal::Device::new_from_raw_mtl(device, &queue);
        let session = Session::new(piet_device);
        let query_pool = session.create_query_pool(12).unwrap();
        Self {
            session,
            pgpu_renderer: None,
            query_pool,
            width: 0,
            height: 0,
            is_color: false,
        }
    }

    #[cfg(all(
        not(target_arch = "wasm32"),
        any(target_os = "ios", target_os = "macos")
    ))]
    pub fn render(
        &mut self,
        scene: &PgpuScene,
        cmdbuf: &metal::CommandBufferRef,
        target: &metal::TextureRef,
    ) -> u32 {
        let is_color = target.pixel_format() != metal::MTLPixelFormat::A8Unorm;
        let width = target.width() as u32;
        let height = target.height() as u32;
        if self.pgpu_renderer.is_none()
            || self.width != width
            || self.height != height
            || self.is_color != is_color
        {
            self.width = width;
            self.height = height;
            self.is_color = is_color;
            let format = if is_color {
                PixelFormat::Rgba8
            } else {
                PixelFormat::A8
            };
            let config = RenderConfig::new(width as usize, height as usize).pixel_format(format);
            unsafe {
                self.pgpu_renderer =
                    piet_gpu::Renderer::new_from_config(&self.session, config, 1).ok();
            }
        }
        unsafe {
            let mut cmd_buf = self.session.cmd_buf_from_raw_mtl(cmdbuf);
            let dst_image = self
                .session
                .image_from_raw_mtl(target, self.width, self.height);
            if let Some(renderer) = &mut self.pgpu_renderer {
                renderer.upload_scene(&scene.encoded_scene(), 0).unwrap();
                renderer.record(&mut cmd_buf, &self.query_pool, 0);
                // TODO later: we can bind the destination image and avoid the copy.
                cmd_buf.blit_image(&renderer.image_dev, &dst_image);
            }
        }
        0
    }

    pub fn release(&mut self, _id: u32) {
        // TODO: worry about freeing resources / managing overlapping submits
    }
}

/// Encoded streams and resources describing a vector graphics scene.
pub struct PgpuScene {
    scene: Scene,
    rcx: ResourceContext,
}

impl PgpuScene {
    pub fn new() -> Self {
        Self {
            scene: Scene::default(),
            rcx: ResourceContext::new(),
        }
    }

    pub fn build(&mut self) -> PgpuSceneBuilder {
        self.rcx.advance();
        PgpuSceneBuilder(piet_scene::scene::build_scene(
            &mut self.scene,
            &mut self.rcx,
        ))
    }

    fn encoded_scene<'a>(&'a self) -> EncodedSceneRef<'a, piet_scene::geometry::Affine> {
        let d = self.scene.data();
        EncodedSceneRef {
            transform_stream: &d.transform_stream,
            tag_stream: &d.tag_stream,
            pathseg_stream: &d.pathseg_stream,
            linewidth_stream: &d.linewidth_stream,
            drawtag_stream: &d.drawtag_stream,
            drawdata_stream: &d.drawdata_stream,
            n_path: d.n_path,
            n_pathseg: d.n_pathseg,
            n_clip: d.n_clip,
            ramp_data: self.rcx.ramp_data(),
        }
    }
}

/// Builder for constructing an encoded scene.
pub struct PgpuSceneBuilder<'a>(piet_scene::scene::Builder<'a>);

impl<'a> PgpuSceneBuilder<'a> {
    pub fn add_glyph(&mut self, glyph: &PgpuGlyph, transform: &piet_scene::geometry::Affine) {
        self.0.push_transform(*transform);
        self.0.append(&glyph.fragment);
        self.0.pop_transform();
    }

    pub fn finish(self) {
        self.0.finish();
    }
}

/// Tag and value for a font variation axis.
#[derive(Copy, Clone)]
#[repr(C)]
pub struct PgpuFontVariation {
    /// Tag that specifies the axis.
    pub tag: u32,
    /// Requested setting for the axis.
    pub value: f32,
}

/// Context for loading and scaling glyphs.
pub struct PgpuGlyphContext(GlyphContext);

impl PgpuGlyphContext {
    pub fn new() -> Self {
        Self(GlyphContext::new())
    }

    pub fn new_provider<'a>(
        &'a mut self,
        font_data: &'a [u8],
        font_index: u32,
        font_id: u64,
        ppem: f32,
        hint: bool,
        variations: &[PgpuFontVariation],
    ) -> Option<PgpuGlyphProvider> {
        let font = FontDataRef::new(font_data).and_then(|f| f.get(font_index))?;
        Some(PgpuGlyphProvider(
            self.0.new_provider(
                &font,
                Some(font_id),
                ppem,
                hint,
                variations
                    .iter()
                    .map(|variation| (Tag(variation.tag), variation.value)),
            ),
        ))
    }
}

/// Context for loading a scaling glyphs from a specific font.
pub struct PgpuGlyphProvider<'a>(GlyphProvider<'a>);

impl<'a> PgpuGlyphProvider<'a> {
    pub fn get(&mut self, gid: u16) -> Option<PgpuGlyph> {
        let fragment = self.0.get(gid)?;
        Some(PgpuGlyph { fragment })
    }

    pub fn get_color(&mut self, palette_index: u16, gid: u16) -> Option<PgpuGlyph> {
        let fragment = self.0.get_color(palette_index, gid)?;
        Some(PgpuGlyph { fragment })
    }
}

/// Encoded (possibly color) outline for a glyph.
pub struct PgpuGlyph {
    fragment: Fragment,
}

impl PgpuGlyph {
    pub fn bbox(&self, transform: Option<Affine>) -> Rect {
        if let Some(transform) = &transform {
            Rect::from_points(
                self.fragment
                    .points()
                    .iter()
                    .map(|p| p.transform(transform)),
            )
        } else {
            Rect::from_points(self.fragment.points())
        }
    }
}