//    _ \                   | _)               
//   |   |   __|  _ \    _` |  |   _` |  |   | 
//   ___/   |    (   |  (   |  |  (   |  |   | 
//  _|     _|   \___/  \__,_| _| \__, | \__, | 
//                               |___/   ____/ 
//
// SPDX-License-Identifier: MIT
// Copyright Han, 2023

pragma solidity ^0.8.25;

contract Prodigy {
    event ArtpieceCreated(address indexed creator);
    event ArtpieceTransferred(address indexed oldOwner, address indexed newOwner);
    event BidAccepted(uint256 value, address indexed fromAddress, address indexed toAddress);
    event BidPlaced(uint256 value, address indexed fromAddress);
    event BidWithdrawn(uint256 value, address indexed fromAddress);
    event ListedForSale(uint256 value, address indexed fromAddress, address indexed toAddress);
    event SaleCanceled(uint256 value, address indexed fromAddress, address indexed toAddress);
    event SaleCompleted(uint256 value, address indexed fromAddress, address indexed toAddress);

    error FundsTransfer();
    error InsufficientFunds();
    error ListedForSaleToSpecificAddress();
    error NoBid();
    error NotForSale();
    error NotOwner();
    error NotRoyaltyRecipient();
    error NotYourBid();
    error NullAddress();
    error RoyaltyTooHigh();

    string public constant MANIFEST = (
        'Am I a criminal now?' '\n'
    );

    string public constant CORE = (
        '"use strict";const credits="wwwtyro sphere intersect, iq pyramid, bigwings normal";let s={signature:"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjIxNiIgdmlld0JveD0iMCAwIDIxNiAyMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0wIDBIMTZWMTZIMFYwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTAgMjAwSDE2VjIxNkgwVjIwMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMTYgMEgyMDBWMTZIMjE2VjBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOTYgOTZIMTIwVjEyMEg5NlY5NloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMTYgMjAwSDIwMFYyMTZIMjE2VjIwMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zMiAwSDE4NFYxNkgzMlYwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE4NCAyMDBIMzJWMjE2SDE4NFYyMDBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjE2IDMyVjE4NEgyMDBWMzJIMjE2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE2IDE4NFYzMkgwVjE4NEgxNloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=",mouse_sensitivity:1,mouse_limit:.4,color_offset_frames:60,frame:0,res:[0,0],save_frames:0},h={newline:String.fromCharCode(10),parser:new URL(window.location)};const mobile=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);let w=window,d=document;d.body.style.touchAction="none",d.body.style.userSelect="none";let b=d.body,wrapper=d.createElement("div");wrapper.style.cssText="position: relative; margin: 0 auto",d.body.appendChild(wrapper);let c=d.createElement("canvas");c.style.display="block",wrapper.appendChild(c);const image=d.createElement("img");image.src=s.signature.trim(),image.style.cssText="width:40px;z-index:50;position:absolute;bottom:20px;right:20px;",wrapper.appendChild(image);const glOptions={powerPreference:"high-performance"};mobile&&delete glOptions.powerPreference,w.gl=c.getContext("webgl2",glOptions),h.uniform1i=(e,r)=>h.uniform(e,r,"uniform1i"),h.uniform=(e,r,t)=>{s.uniforms||={};(s.uniforms[e]||=(()=>{const i=gl.getUniformLocation(s.current_program,e),o=t||(Array.isArray(r)?"uniform2fv":"uniform1f");return{update:e=>gl[o](i,e)}})()).update(r)},h.resize=()=>{let e,r,t,i={x:h.ix.mouse.x/s.res[0],y:h.ix.mouse.y/s.res[1]};const o=h.parser.searchParams.get("res",r);o?(e=r=o,t=1):(e=w.innerWidth,r=w.innerHeight,s.aspect&&(e>r*s.aspect?e=r*s.aspect:r=e/s.aspect),t=w.devicePixelRatio),s.res[0]=c.width=e*t,s.res[1]=c.height=r*t,wrapper.style.width=c.style.width=e+"px",wrapper.style.height=c.style.height=r+"px",h.ix.set(c.width*i.x,c.height*i.y)},h.ix={start:{x:0,y:0},mouse:{x:0,y:0}},h.ix.set=(e,r)=>{h.ix.mouse={x:e,y:r}},h.ix.start=e=>{h.ix.start.x=e.clientX,h.ix.start.y=e.clientY,d.addEventListener("pointermove",h.ix.move)},h.clamp=(e,r,t)=>Math.max(r,Math.min(t,e)),h.ix.move=e=>{h.ix.mouse.x+=(e.clientX-h.ix.start.x)*window.devicePixelRatio*s.mouse_sensitivity,h.ix.mouse.y-=(e.clientY-h.ix.start.y)*window.devicePixelRatio*s.mouse_sensitivity,h.ix.start.x=e.clientX,h.ix.start.y=e.clientY,h.ix.mouse.x=h.clamp(h.ix.mouse.x,s.res[0]*s.mouse_limit,s.res[0]*(1-s.mouse_limit)),h.ix.mouse.y=h.clamp(h.ix.mouse.y,s.res[1]*s.mouse_limit,s.res[1]*(1-s.mouse_limit))},h.ix.stop=()=>{d.removeEventListener("pointermove",h.ix.move)},h.save={},h.save.toImage=()=>{const e=new Date;let r=String(e.getFullYear()).slice(2,4)+"-"+e.getMonth()+"-"+e.getDate()+" ("+s.frame+").png",t=document.createElement("a");t.setAttribute("download",r);let i=c.toDataURL("image/png").replace("data:image/png","data:application/octet-stream");t.setAttribute("href",i),t.click(),t.remove()},h.buildShader=(e,r)=>{let t=gl.createShader(e);return gl.shaderSource(t,r),gl.compileShader(t),t},h.initProgram=(e,r,t)=>{const i=gl.createProgram(),o=h.buildShader(gl.VERTEX_SHADER,r),s=h.buildShader(gl.FRAGMENT_SHADER,e);gl.attachShader(i,o),gl.attachShader(i,s),gl.linkProgram(i),gl.getShaderParameter(o,gl.COMPILE_STATUS)||console.error("V: "+gl.getShaderInfoLog(o)),gl.getShaderParameter(s,gl.COMPILE_STATUS)||console.error("F: "+gl.getShaderInfoLog(s)),gl.getProgramParameter(i,gl.LINK_STATUS)||console.error("P: "+gl.getProgramInfoLog(i));let a=gl.createBuffer(),n=gl.getAttribLocation(i,"p");return gl.bindBuffer(gl.ARRAY_BUFFER,a),gl.bufferData(gl.ARRAY_BUFFER,t,gl.STATIC_DRAW),gl.enableVertexAttribArray(n),gl.vertexAttribPointer(n,2,gl.FLOAT,!1,0,0),i},s.pixel=new Uint8Array(4),h.render=()=>{gl.viewport(0,0,c.width,c.height),gl.useProgram(s.program),s.current_program=s.program,s.frame<s.save_frames?h.uniform("u_time",.01667*s.frame):h.uniform("u_time",.001*performance.now()),h.uniform("u_resolution",s.res),h.uniform("u_mouse",[h.ix.mouse.x,h.ix.mouse.y]),gl.drawArrays(gl.TRIANGLE_STRIP,0,4),gl.readPixels(0,0,1,1,gl.RGBA,gl.UNSIGNED_BYTE,s.pixel),(h.save.queued||s.frame<s.save_frames&&s.frame>3)&&(h.save.queued=!1,h.save.toImage()),s.frame++,requestAnimationFrame(h.render)};const init=async()=>{if(gl){h.resize(),h.ix.set(c.width/2,c.height/2),d.addEventListener("pointerdown",h.ix.start),d.addEventListener("pointerup",h.ix.stop),w.addEventListener("resize",h.resize),s.save_frames=h.parser.searchParams.get("frames")||0,d.addEventListener("keyup",(e=>{if(!(e.ctrlKey||e.altKey||e.shiftKey||e.metaKey))switch(e.key){case"f":const e=prompt("Frames to render",s.save_frames);h.parser.searchParams.set("frames",e),window.location=h.parser.href;break;case"r":const r=prompt("Canvas size in pixels (max 8192)",c.height);r>=8&&r<=8192&&(h.parser.searchParams.set("res",r),window.location=h.parser.href);break;case"s":h.save.queued=!0}}));let e=Float32Array.of(-1,1,-1,-1,1,1,1,-1);const r="#version 300 es"+h.newline+"precision highp float; in vec2 p; void main(){ gl_Position =vec4(p,1.0,1.0); }";let t="precision highp float;out vec4 fragColor;const int u_aa=2;uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;const vec3 v=vec3(.941176470588235),f=vec3(.745098039215686);const vec3 d=vec3(0,0,-.5);const vec3 m=vec3(.980392156862745),x=vec3(.941176470588235);vec3 n(vec3 v,vec4 u){return v+2.*cross(u.xyz,cross(u.xyz,v)+v*u.w);}vec3 s(vec3 v,float u){return n(v,vec4(0,sin(u/2.),0,cos(u/2.)));}vec3 t(vec3 v,float u){return n(v,vec4(0,0,sin(u/2.),cos(u/2.)));}float n(vec3 v){if(v.y<=0.)return length(max(abs(v)-vec3(.5,0,.5),0.));float f,u;v.xz=abs(v.xz);v.xz=v.z>v.x?v.zx:v.xz;v.xz-=.5;vec3 d=vec3(v.z,v.y-.5*v.x,v.x+.5*v.y);f=max(-d.x,0.);u=clamp((d.y-.5*d.x)/1.5,0.,1.);return sqrt(((max(-d.y,d.x*1.25+d.y*.5)<0.?0.:min(1.25*(d.x+f)*(d.x+f)+d.y*d.y,1.25*(d.x+.5*u)*(d.x+.5*u)+(d.y-1.25*u)*(d.y-1.25*u)))+d.z*d.z)/1.25)*sign(max(d.z,-v.y));}vec2 u(vec3 v,float u){float f=1e2,d,x,y,z,r,c,m,i;v=t(v,u);d=.12;x=mix(0.,.2,.5+.5*sin(u*2.));x+=1.+d;vec3 a=v-vec3(0,-x-.15,0),g,e,k;a.x*=.675;a=s(a,-u*3.);y=n(a)-.15;f=min(f,y);g=-v-vec3(0,-x-.15,0);g.x*=.675;g=s(g,u*3.);z=n(g)-.15;f=min(f,z);e=v;e-=vec3(-x-.15,0,0);e.y*=.675;e=t(e,1.570795);e=s(e,u*3.);r=n(e)-.15;f=min(f,r);k=v;k-=vec3(x+.15,0,0);k.y*=.675;k=t(k,-1.570795);k=s(k,u*3.);c=n(k)-.15;f=min(f,c);m=length(v-vec3(0))-d;f=min(f,m);i=1.;if(f==m)i=2.;return vec2(f,i);}vec2 n(vec3 v,vec3 f,float d){float x=0.,r=-1.;for(int e=0;e<128;e++){vec3 m=v+x*f;vec2 n=u(m,d);float i=n.x;r=n.y;if(i<.001||x>1e2)break;x+=i;}return vec2(x,r);}vec3 e(vec3 v,float f){vec2 d=vec2(.001,0);vec3 m=u(v,f).x-vec3(u(v-d.xyy,f).x,u(v-d.yxy,f).x,u(v-d.yyx,f));return normalize(m);}vec3 e(){vec3 v=gl_FragCoord.xyy,f;v=vec3(dot(v,vec3(127.1,311.7,74.7)),dot(v,vec3(269.5,183.3,246.1)),dot(v,vec3(113.5,271.9,124.6)));f=-1.+2.*fract(sin(v)*43758.5453123);return fract(555.*sin(777.*f))/256.;}vec3 e(vec3 v){v+=e();return v;}float s(vec3 v){v=fract(v*.3183099+.1);v*=17.;return fract(v.x*v.y*v.z*(v.x+v.y+v.z));}float t(vec3 v){vec3 f=floor(v),d=fract(v);d=d*d*(3.-2.*d);return mix(mix(mix(s(f+vec3(0)),s(f+vec3(1,0,0)),d.x),mix(s(f+vec3(0,1,0)),s(f+vec3(1,1,0)),d.x),d.y),mix(mix(s(f+vec3(0,0,1)),s(f+vec3(1,0,1)),d.x),mix(s(f+vec3(0,1,1)),s(f+vec3(1)),d.x),d.y),d.z);}vec3 e(inout vec3 u,inout vec3 y,float z,float s){vec2 i=n(u,y,s);vec3 r=vec3(0);if(i.x>=1e2){vec2 a=(gl_FragCoord.xy-.5*u_resolution.xy)*.0968287165617335/min(u_resolution.x,u_resolution.y);float c=t(vec3(a*0.,cos(s*0.)));c*=1.01;c=pow(c,1.33);vec3 k=mix(m,x,c);r=k;}else{vec3 a=u+i.x*y,c=e(u+i.x*y,s),k,g,o;float h=1.2*pow(1.-dot(c,-y),2.),w,C;h=max(0.,h);vec2 l=vec2(u_mouse/u_resolution);k=vec3(60.*(l.x-.5),-60.*(l.y-.5),0);w=4.*(s-2.*acos(-1.)*z);g=k+vec3(cos(w),sin(w),d.z)*10.;o=normalize(g-a);C=max(0.,dot(c,o));r.x=mix(f,v,C+h).x;w=4.*s;g=k+vec3(cos(w),sin(w),d.z)*10.;o=normalize(g-a);C=max(0.,dot(c,o));r.y=mix(f,v,C+h).y;w=4.*(s+2.*acos(-1.)*z);g=k+vec3(cos(w),sin(w),d.z)*10.;o=normalize(g-a);C=max(0.,dot(c,o));r.z=mix(f,v,C+h).z;}return r;}vec3 r(float v,float f){float u=sin(f);return vec3(12.*u*cos(v),12.*cos(f),12.*u*sin(v));}vec3 r(vec2 v){return r(-2.*acos(-1.)*v.y-1.570795,v.x*3.14159);}mat3 h(vec3 v,vec2 d){vec3 f=normalize(vec3(0)-v),u=normalize(cross(vec3(0,floor(mod(d.y,2.))==0.?-1.:1.,0),f));return mat3(u,normalize(cross(f,u)),f);}void main(){float v=u_time,f,d;vec2 u=vec2(.5),c;vec3 m=r(u),x;mat3 a=h(m,u);c=gl_FragCoord.xy;f=.5+.5*sin(c.x*147.)*sin(c.y*131.);d=.4*smoothstep(0.,1.,2.*sin(2.*acos(-1.)*v/(20./3.)));v+=0.;int y=int(u_aa);x=vec3(0);for(int g=0;g<y;g++)for(int i=0;i<y;i++){vec2 k=vec2(g,i)/float(u_aa)-.5,z=(gl_FragCoord.xy+k-.5*u_resolution.xy)*.0968287165617335/min(u_resolution.x,u_resolution.y);vec3 s=normalize(a*vec3(z,1));float o=v-.125*(float(i*y+g)+f)/float(y*y),w;o/=20.;w=2.*acos(-1.)*o;x+=e(m,s,d/20.,w);}x/=float(u_aa*u_aa);x=e(x);fragColor=vec4(x,1);}";if(t="#version 300 es"+h.newline+t,mobile){const e="const int u_aa=2";t=t.replace(e,"const int u_aa=1")}window.program=s.program=h.initProgram(t,r,e),h.render()}else{const e=d.createElement("div");e.style.cssText="align-items:center;background:#969696;color:#fff;display:flex;font-family:monospace;font-size:20px;height:100vh;justify-content:center;left:0;position:fixed;top:0;width:100vw;",e.innerHTML="Your browser does not support WebGL.",b.append(e)}};init();'
    );

    modifier onlyOwner() {
        if (owner != msg.sender) {
            revert NotOwner();
        }

        _;
    }

    modifier onlyRoyaltyRecipient() {
        if (royaltyRecipient != msg.sender) {
            revert NotRoyaltyRecipient();
        }

        _;
    }

    struct Offer {
        bool active;
        uint256 value;
        address toAddress;
    }

    struct Bid {
        bool active;
        uint256 value;
        address fromAddress;
    }

    address public owner;

    Offer public currentOffer;

    Bid public currentBid;

    address public royaltyRecipient;

    uint256 public royaltyPercentage;

    mapping (address => uint256) public pendingWithdrawals;

    constructor(uint256 _royaltyPercentage) {
        if (_royaltyPercentage >= 100) {
            revert RoyaltyTooHigh();
        }

        owner = msg.sender;
        royaltyRecipient = msg.sender;
        royaltyPercentage = _royaltyPercentage;

        emit ArtpieceCreated(msg.sender);
    }

    function name() public view virtual returns (string memory) {
        return 'Prodigy';
    }

    function symbol() public view virtual returns (string memory) {
        return 'P';
    }

    function artpiece() public view virtual returns (string memory) {
        return string.concat(
            '<!DOCTYPE html>'
            '<html>'
                '<head>'
                    '<title>', 'Prodigy', '</title>'

                    '<meta name="viewport" content="width=device-width, initial-scale=1" />'

                    '<style>html,body{background:#969696;margin:0;padding:0;overflow:hidden;}</style>'
                '</head>'

                '<body>'
                    '<script type="text/javascript">',
                        CORE,
                    '</script>'
                '</body>'
            '</html>'
        );
    }

    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];

        pendingWithdrawals[msg.sender] = 0;

        _sendFunds(amount);
    }

    function transferOwnership(address newOwner) public onlyOwner {
        if (newOwner == address(0)) {
            revert NullAddress();
        }

        _transferOwnership(newOwner);

        if (currentBid.fromAddress == newOwner) {
            uint256 amount = currentBid.value;

            currentBid = Bid({ active: false, value: 0, fromAddress: address(0) });

            pendingWithdrawals[newOwner] += amount;
        }

        if (currentOffer.active) {
            currentOffer = Offer({ active: false, value: 0, toAddress: address(0) });
        }
    }

    function listForSale(uint256 salePriceInWei) public onlyOwner {
        currentOffer = Offer({ active: true, value: salePriceInWei, toAddress: address(0) });

        emit ListedForSale(salePriceInWei, msg.sender, address(0));
    }

    function listForSaleToAddress(uint256 salePriceInWei, address toAddress) public onlyOwner {
        currentOffer = Offer({ active: true, value: salePriceInWei, toAddress: toAddress });

        emit ListedForSale(salePriceInWei, msg.sender, toAddress);
    }

    function cancelFromSale() public onlyOwner {
        Offer memory oldOffer = currentOffer;

        currentOffer = Offer({ active: false, value: 0, toAddress: address(0) });

        emit SaleCanceled(oldOffer.value, msg.sender, oldOffer.toAddress);
    }

    function buyNow() public payable {
        if (!currentOffer.active) {
            revert NotForSale();
        }

        if (currentOffer.toAddress != address(0) && currentOffer.toAddress != msg.sender) {
            revert ListedForSaleToSpecificAddress();
        }

        if (msg.value != currentOffer.value) {
            revert InsufficientFunds();
        }

        currentOffer = Offer({ active: false, value: 0, toAddress: address(0) });

        uint256 royaltyAmount = _calcRoyalty(msg.value);

        pendingWithdrawals[owner] += msg.value - royaltyAmount;
        pendingWithdrawals[royaltyRecipient] += royaltyAmount;

        emit SaleCompleted(msg.value, owner, msg.sender);

        _transferOwnership(msg.sender);
    }

    function placeBid() public payable {
        if (msg.value <= currentBid.value) {
            revert InsufficientFunds();
        }

        if (currentBid.value > 0) {
            pendingWithdrawals[currentBid.fromAddress] += currentBid.value;
        }

        currentBid = Bid({ active: true, value: msg.value, fromAddress: msg.sender });

        emit BidPlaced(msg.value, msg.sender);
    }

    function acceptBid() public onlyOwner {
        if (!currentBid.active) {
            revert NoBid();
        }

        uint256 amount = currentBid.value;
        address bidder = currentBid.fromAddress;

        currentOffer = Offer({ active: false, value: 0, toAddress: address(0) });
        currentBid = Bid({ active: false, value: 0, fromAddress: address(0) });

        uint256 royaltyAmount = _calcRoyalty(amount);

        pendingWithdrawals[owner] += amount - royaltyAmount;
        pendingWithdrawals[royaltyRecipient] += royaltyAmount;

        emit BidAccepted(amount, owner, bidder);

        _transferOwnership(bidder);
    }

    function withdrawBid() public {
        if (msg.sender != currentBid.fromAddress) {
            revert NotYourBid();
        }

        uint256 amount = currentBid.value;

        currentBid = Bid({ active: false, value: 0, fromAddress: address(0) });

        _sendFunds(amount);

        emit BidWithdrawn(amount, msg.sender);
    }

    function setRoyaltyRecipient(address newRoyaltyRecipient) public onlyRoyaltyRecipient {
        if (newRoyaltyRecipient == address(0)) {
            revert NullAddress();
        }

        royaltyRecipient = newRoyaltyRecipient;
    }

    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = owner;

        owner = newOwner;

        emit ArtpieceTransferred(oldOwner, newOwner);
    }

    function _sendFunds(uint256 amount) internal virtual {
        (bool success, ) = msg.sender.call{value: amount}('');

        if (!success) {
            revert FundsTransfer();
        }
    }

    function _calcRoyalty(uint256 amount) internal virtual returns (uint256) {
        return (amount * royaltyPercentage) / 100;
    }
}
