// SPDX-License-Identifier: MPL-2.0
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <exception>
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>
#include <filesystem>
#include <random>
#include <fstream>
using namespace std;
namespace fs=filesystem;
struct point {
  long double x;
  long double y;
};
struct pixel {
  uint16_t x;
  uint16_t y;
};
const point AP0={0,0};
const point AP1={0.35,0.05};
const point AP2={0.65,1.0};
const point AP3={1,1};
const point BP0={0,0};
const point BP1={0.1,1.5};
const point BP2={0.3,1.0};
const point BP3={1,1};
long double bezier_1d(long double p0,long double p1,long double p2,long double p3,long double t) {
  long double u=1.0-t;
  return (u*u*u)*p0+3*(u*u)*t*p1+3*u*(t*t)*p2+(t*t*t)*p3;
}
long double find_t(long double p0,long double p1,long double p2,long double p3,long double x_search) {
  long double low=0.0;
  long double high=1.0;
  long double mid;
  for (int i=0;i<20;i++) {
    mid=(low+high)/2.0;
    long double x_mid=bezier_1d(p0,p1,p2,p3,mid);
    if (x_mid<x_search)
      low=mid;
    else
      high=mid;
  }
  return (low+high)/2.0;
}
vector<point> apply_bezier(point start,point p0,point p1,point p2,point p3,point end,int frames) {
  point vect;
  vect.x=end.x-start.x;
  vect.y=end.y-start.y;
  vector<point> out;
  out.reserve(frames);
  for (int frame=0;frame<frames;frame++) {
    long double t_brut=frame/(long double)(frames-1);
    long double t_interne=find_t(p0.x,p1.x,p2.x,p3.x,t_brut);
    long double progression=bezier_1d(p0.y,p1.y,p2.y,p3.y,t_interne);
    long double pos_x=start.x+vect.x*progression;
    long double pos_y=start.y+vect.y*progression;
    point p;
    p.x=pos_x;
    p.y=pos_y;
    out.push_back(p);
  }
  return out;
}
void save_ppm(const string& path,int w,int h,vector<pixel>& white_pixels) {
  vector<unsigned char> img(w*h*3,0);
  for (auto& p:white_pixels) {
    if (p.x<w-1 && p.y<h-1 && p.x!=0 && p.y!=0) {
      int idx=(p.y*w+p.x)*3;
      img[idx+0]=255;
      img[idx+1]=255;
      img[idx+2]=255;
    } else {
      int idx=(p.y*w+p.x)*3;
      img[idx+0]=0;
      img[idx+1]=0;
      img[idx+2]=0;
    }
  }
  ofstream out(path,ios::binary);
  out<<"P6\n"<<w<<" "<<h<<"\n255\n";
  out.write((char*)img.data(),img.size());
  out.close();
}
uint16_t clamp(long double v,uint16_t max) {
  if (v<0) return (uint16_t)0;
  if (v>max) return max;
  return (uint16_t)v;
}
int main (int argc,char **argv) {
  if (argc!=5) {
    cout<<"[Bootanim] Error: invalid argument."<<endl;
    return -1;
  }
  string logopath=string(argv[1]);
  if (!fs::exists(logopath)) {
    cout<<"[Bootanim] Error: provided logo doesn't exist."<<endl;
    return -1;
  }
  bool export_file;
  if (string(argv[2])=="file") {
    export_file=true;
  } else if (string(argv[2])=="folder") {
    export_file=false;
  } else {
    cout<<"[Bootanim] Error: invalid export mode."<<endl;
    return -1;
  }
  string widthstr=string(argv[3]);
  int width;
  try {
    width=stoi(widthstr);
  } catch (exception const& e) {
    cout<<"[Bootanim] Error: invalid width provided."<<endl;
    return -1;
  }
  string heightstr=string(argv[4]);
  int height;
  try {
    height=stoi(heightstr);
  } catch (exception const& e) {
    cout<<"[Bootanim] Error: invalid height provided."<<endl;
    return -1;
  }
  long double ratio=(long double)width/(long double)height;
  int w_image,h_image,c;
  unsigned char *logodata=stbi_load(logopath.c_str(),&w_image,&h_image,&c,1);
  if (!logodata) {
    cout<<"[Bootanim] Error: can't load logo file."<<endl;
    return -1;
  }
  if (w_image!=h_image) {
    cout<<"[Bootanim] Error: image isn't square."<<endl;
    return -1;
  }
  int true_w=w_image+(width-w_image);
  int true_h=h_image+(height-h_image);
  vector<unsigned char> logopixels(true_w*true_h,0);
  int offset_x=(true_w-w_image)/2;
  int offset_y=(true_h-h_image)/2;
  for (int y=0;y<h_image;y++) {
    for (int x=0;x<w_image;x++) {
      int dst_x=x+offset_x;
      int dst_y=y+offset_y;
      if (dst_x>=0 && dst_x<true_w && dst_y>=0 && dst_y<true_h) {
        int src=y*w_image+x;
        int dst=dst_y*true_w+dst_x;
        logopixels[dst]=logodata[src];
      }
    }
  }
  stbi_image_free(logodata);
  vector<point> endco;
  for (int i=0;i<logopixels.size();++i) {
    if (logopixels[i]>127) {
      point p;
      p.x=i%true_w;
      p.y=i/true_w;
      endco.push_back(p);
    }
  }
  random_device rd;
  mt19937 gen(rd());
  vector<point> randco;
  randco.reserve(endco.size());
  uniform_real_distribution<long double> dist_angle(0.0L,2.0L*M_PI);
  uniform_real_distribution<long double> dist_r1(0.0L,min((long double)true_w,(long double)true_h)/2);
  uniform_real_distribution<long double> dist_r2(min((long double)true_w,(long double)true_h)/2,(max((long double)true_w,(long double)true_h)+300)/2);
  uniform_int_distribution<int> dist_c(0,1);
  long double cx=(long double)true_w/2.0;
  long double cy=(long double)true_h/2.0;
  for (int i=0;i<endco.size();i++) {
    long double a=dist_angle(gen);
    long double r;
    if (dist_c(gen)==1) {
        r=dist_r1(gen);
    } else {
        r=dist_r2(gen);
    }
    point p;
    p.x=cx+cos(a)*r;
    p.y=cy+sin(a)*r;
    randco.push_back(p);
  }
  vector<vector<point>> codata(301);
  for (int i=0;i<endco.size();++i) {
    point p={(long double)true_w/2,(long double)true_h/2};
    codata[0].push_back(p);
  }
  vector<vector<point>> tempdata1;
  vector<vector<point>> tempdata2;
  tempdata1.reserve(endco.size());
  tempdata2.reserve(endco.size());
  for (int i=0;i<endco.size();++i) {
    tempdata1.push_back(apply_bezier(codata[0][i],BP0,BP1,BP2,BP3,randco[i],40));
    tempdata2.push_back(apply_bezier(randco[i],AP0,AP1,AP2,AP3,endco[i],40));
  }
  for (int i=1;i<40;i++) {
    for (int y=0;y<tempdata1.size();++y) {
      codata[i].push_back(tempdata1[y][i]);
    }
  }
  for (int i=0;i<40;i++) {
    for (int y=0;y<tempdata2.size();++y) {
      codata[i+40].push_back(tempdata2[y][i]);
    }
  }
  vector<vector<pixel>> pixeldata(81);
  for (int i=0;i<80;i++) {
    for (int y=0;y<codata[i].size();++y) {
      pixel p;
      p.x=clamp(codata[i][y].x,width-1);
      p.y=clamp(codata[i][y].y,height-1);
      pixeldata[i].push_back(p);
    }
  }
  for (int i=0;i<endco.size();++i) {
    pixel p;
    p.x=clamp(endco[i].x,width-1);
    p.y=clamp(endco[i].y,height-1);
    pixeldata[pixeldata.size()-1].push_back(p);
  }
  if (!export_file) {
    if (!fs::exists("frames")) {
      fs::create_directory("frames");
    } else if (!fs::is_directory("frames")) {
      cout<<"[Bootanim] Error: 'frames' already exists and isn't a folder."<<endl;
      return -1;
    }
    for (int i=0;i<pixeldata.size();i++) {
      char name[64];
      sprintf(name,"./frames/frame%04d.ppm",i);
      save_ppm(name,true_w,true_h,pixeldata[i]);
    }
    system(string("ffmpeg -framerate 16 -i frames/frame%04d.ppm -pix_fmt yuv420p out.mp4").c_str());
    cout<<"[Bootanim] Successfully exported video as out.mp4."<<endl;
  } else {
    vector<unsigned char> data(pixeldata.size()*pixeldata[0].size()*sizeof(pixel));
    size_t offset=0;
    for (size_t i=0;i<pixeldata.size();++i) {
      for (size_t p=0;p<pixeldata[i].size();++p) {
        pixel& px=pixeldata[i][p];
        if (px.x>=width-1 || px.x==0) {
            px.x=0xFFFF;
        }
        if (px.y>=height-1 || px.y==0) {
            px.y=0xFFFF;
        }
        memcpy(&data[offset],&px,sizeof(pixel));
        offset+=sizeof(pixel);
      }
    }
    ofstream file("bootanim.bin",ios::binary);
    unsigned char magic[8]={'B','o','o','t','A','n','i','m'};
    file.write((char*)magic,8);
    vector<uint64_t> header(4);
    header[0]=width;
    header[1]=height;
    header[2]=pixeldata.size();
    header[3]=pixeldata[0].size();
    vector<unsigned char> headerdata(4*8);
    memcpy(headerdata.data(),header.data(),headerdata.size());
    file.write((char *)headerdata.data(),headerdata.size());
    file.write((char *)data.data(),data.size());
    file.close();
    cout<<"[Bootanim] Successfully build boot animation as bootanim.bin."<<endl;
  }
  return 0;
}
