import React, { useState, useEffect, useRef, useCallback, 
  useContext } from "react";
import Moveable from "react-moveable";

import "./MediaControl.css";

import VolumeOff from '@mui/icons-material/VolumeOff';
import VolumeUp from '@mui/icons-material/VolumeUp';
import MicOff from '@mui/icons-material/MicOff';
import Mic from '@mui/icons-material/Mic';
import VideocamOff from '@mui/icons-material/VideocamOff';
import Videocam from '@mui/icons-material/Videocam';

import { GlobalContext } from "./GlobalContext.js";
const debug = true;

/* Proxy object so we can pass in srcObject to <audio> */
const Video = ({ srcObject, local, ...props }) => {
  const refVideo = useRef(null);
  useEffect(() => {
    if (!refVideo.current) {
      return;
    }
    const ref = refVideo.current;
    if (debug) console.log('media-control - video <video> bind');
    ref.srcObject = srcObject;
    if (local) {
      ref.muted = true;
    }
    return () => {
      if (debug) console.log('media-control - <video> unbind');
      if (ref) {
        ref.srcObject = undefined;
      }
    }; 
  }, [srcObject, local]);
  return <video ref={refVideo} {...props} />;
}

const MediaAgent = ({setPeers}) => {
  const { name, ws } = useContext(GlobalContext);
  const [ peers ] = useState({});
  const [stream, setStream] = useState(undefined);

  const onTrack = useCallback((event) => {
    const connection = event.target;
    console.log("media-agent - ontrack", event);
    for (let peer in peers) {
      if (peers[peer].connection === connection) {
        console.log(`media-agent - ontrack - remote ${peer} stream assigned.`);
        Object.assign(peers[peer].attributes, {
          srcObject: event.streams[0]
        });
        /* Trigger update of MediaControl now that a stream is available */
        setPeers(Object.assign({}, peers));
      }
    }
  }, [peers, setPeers]);
  const refOnTrack = useRef(onTrack);

  const sendMessage = useCallback((data) => {
    ws.send(JSON.stringify(data));
  }, [ws]);

  const onWsMessage = useCallback((event) => {
     const addPeer = (config) => {
      console.log('media-agent - Signaling server said to add peer:', config);
  
      if (!stream) {
        console.log(`media-agent - No local media stream`);
        return;
      }
  
      const peer_id = config.peer_id;
      if (peer_id in peers) {
        if (!peers[peer_id].dead) {
          /* This is normal when peers are added by other connecting 
           * peers through the signaling server */
          console.log(`media-agent - addPeer - ${peer_id} already in peers`);
          return;
        }
      }
       /* Even if reviving, allocate a new Object so <MediaControl> will
      * have its peer state change and trigger an update from 
      * <PlayerList> */
      const peer = {
        name: peer_id,
        hasAudio: config.hasAudio,
        hasVideo: config.hasVideo,
        attributes: {},
      };
    
      if (peer_id in peers) {
        peer.muted = peers[peer_id].muted;
        peer.videoOn = peers[peer_id].videoOn;
        console.log(`media-agent - addPeer - reviving dead peer ${peer_id}`, peer);
      } else {
        peer.muted = false;
        peer.videoOn = true;
      }
      peers[peer_id] = peer;
   
      console.log(`media-agent - addPeer - remote`, peers);
      setPeers(Object.assign({}, peers));
      
      const connection = new RTCPeerConnection({
        configuration: {
          offerToReceiveAudio: true,
          offerToReceiveVideo: true
        },
        iceServers: [{
          urls: "turns:ketrenos.com:5349",
          username: "ketra",
          credential: "ketran"
        },
          /*
          {
            urls: "turn:numb.viagenie.ca",
            username: "james_viagenie@ketrenos.com",
            credential: "1!viagenie"
          } 
          */
        ]
      });
      peer.connection = connection;

      connection.addEventListener('connectionstatechange', (event) => {
        console.log(`media-agent - connectionstatechange - `,
          connection.connectionState, event);
      });

      connection.addEventListener('negotiationneeded', (event) => {
        console.log(`media-agent - negotiationneeded - `,
          connection.connectionState, event);
      });
      
      connection.addEventListener('icecandidateerror', (event) => {
        if (event.errorCode === 701) {
          if (connection.iceGatheringState === 'gathering') {
            console.log(`media-agent - Unable to reach host: ${event.url}`);
          } else {
            console.error(`media-agent - icecandidateerror - `, event.errorCode, event.hostcandidate, event.url, event.errorText);
          }
        }
      });

      connection.onicecandidate = (event) => {
        if (!event.candidate) {
          console.log(`media-agent - icecanditate - gathering is complete: ${connection.connectionState}`);
          return;
        }
        /* If a srflx candidate was found, notify that the STUN server works! */
        if (event.candidate.type === "srflx"){
          console.log("media-agent - The STUN server is reachable!");
          console.log(`media-agent -    Your Public IP Address is: ${event.candidate.address}`);
        }

        /* If a relay candidate was found, notify that the TURN server works! */
        if (event.candidate.type === "relay"){
          console.log("media-agent - The TURN server is reachable !");
        }

        console.log(`media-agent - onicecandidate - `, event.candidate);

        sendMessage({
          type: 'relayICECandidate',
          config: {
            peer_id,
            candidate: event.candidate
          }
        });
      };

      connection.ontrack = e => refOnTrack.current(e);

      /* Add our local stream */
      connection.addStream(stream.media);
  
      /* Only one side of the peer connection should create the
       * offer, the signaling server picks one to be the offerer. 
       * The other user will get a 'sessionDescription' event and will
       * create an offer, then send back an answer 'sessionDescription'
       * to us
       */
      if (config.should_create_offer) {
        if (debug) console.log(`media-agent - Creating RTC offer to ` +
          `${peer_id}`);
        return connection.createOffer()
        .then((local_description) => { 
          if (debug) console.log(`media-agent - Local offer ` +
            `description is: `, local_description);
          return connection.setLocalDescription(local_description)
          .then(() => { 
            sendMessage({
              type: 'relaySessionDescription', 
              config: { 
                peer_id,
                'session_description': local_description
              }
            });
            if (debug) console.log(`media-agent - Offer ` +
              `setLocalDescription succeeded`);
          })
          .catch((error) => {
            console.error(`media-agent - Offer setLocalDescription failed!`); 
          });
        })
        .catch((error) => {
          console.log(`media-agente - Error sending offer: `, error);
        });
      }
    }
    
    const sessionDescription = ({ peer_id, session_description }) => {
      const peer = peers[peer_id];
      if (!peer) {
        console.error(`media-agent - sessionDescription - ` +
          `No peer for ${peer_id}`);
        return;
      }
      const { connection } = peer;
      const desc = new RTCSessionDescription(session_description);
      return connection.setRemoteDescription(desc, () => {
        if (debug) console.log(`media-agent - sessionDescription - ` +
          `setRemoteDescription succeeded`);
        if (session_description.type === "offer") {
          if (debug) console.log(`media-agent - sessionDescription - ` +
            `Creating answer`);
          connection.createAnswer((local_description) => {
            if (debug) console.log(`media-agent - sessionDescription - ` +
              `Answer description is: `, local_description);
            connection.setLocalDescription(local_description, () => { 
              sendMessage({
                type: 'relaySessionDescription',
                config: {
                  peer_id, 
                  session_description: local_description
                }
              });
              if (debug) console.log(`media-agent - sessionDescription ` +
                `- Answer setLocalDescription succeeded`);
            }, () => {
              console.error(`media-agent - sessionDescription - ` +
                `Answer setLocalDescription failed!`);
            });
          }, (error) => {
            console.error(error);
          });
        }
      }, (error) => {
        console.log(`media-agent - sessionDescription - ` +
        `setRemoteDescription error: `, error);
      });
    };
  
    const removePeer = ({peer_id}) => {
      console.log(`media-agent - removePeer - Signaling server said to ` +
        `remove peer ${peer_id}`);
      if (peer_id in peers) {
        if (peers[peer_id].connection) {
          peers[peer_id].connection.close();
          peers[peer_id].connection = undefined;
        }
      }
    
      /* To maintain mute/videoOn states, we don't remove the peer but 
       * instead mark it as dead */
      peers[peer_id].dead = true;
      if (debug) console.log(`media-agent - removePeer`, peers);
      setPeers(Object.assign({}, peers));
    };
  
    const iceCandidate = ({ peer_id, candidate }) => {
      /**
      * The offerer will send a number of ICE Candidate blobs to the
      * answerer so they can begin trying to find the best path to one
      * another on the net.
      */
      const peer = peers[peer_id];
      if (!peer) {
        console.error(`media-agent - iceCandidate - No peer for ` +
          `${peer_id}`, peers);
        return;
      }
      peer.connection.addIceCandidate(new RTCIceCandidate(candidate))
      .then(() => {
        if (debug) console.log(`media-agent - iceCandidate - ` +
          `Successfully added Ice Candidate for ${peer_id}`);
      })
      .catch((error) => {
        console.error(error, peer, candidate);
      });
    };
 
    const data = JSON.parse(event.data);
    if (data.type in [ 'addPeer', 'removePeer', 
      'iceCandidate', 'sessionDescription' ]) {
      console.log(`media-agent - message - ${data.type}`, peers);
    }
    switch (data.type) {
    case 'addPeer': addPeer(data.data); break;
    case 'removePeer': removePeer(data.data); break;
    case 'iceCandidate': iceCandidate(data.data); break;
    case 'sessionDescription': sessionDescription(data.data); break;
    default: break;
    }
  }, [ peers, setPeers, stream, refOnTrack, sendMessage ]);
  const refWsMessage = useRef(onWsMessage);

  const onWsClose = (event) => {
    console.log(`media-agent - ${name} Disconnected from signaling server`);
    /* Tear down all of our peer connections and remove all the
      * media divs when we disconnect */
    for (let peer_id in peers) {
      if (peers[peer_id].local) {
        continue;
      }
      peers[peer_id].connection.close();
      peers[peer_id].connection = undefined;
    }

    for (let id in peers) {
      peers[id].dead = true;
      peers[id].connection = undefined;
    }

    if (debug) console.log(`media-agent - close`, peers);
    setPeers(Object.assign({}, peers));
  }
  const refWsClose = useRef(onWsClose);

  useEffect(() => { 
    refWsMessage.current = onWsMessage; 
    refWsClose.current = onWsClose;
    refOnTrack.current = onTrack;
  });

  useEffect(() => {
    if (!ws) {
      return;
    }
    console.log(`media-control - Binding to WebSocket`);
    const cbMessage = e => refWsMessage.current(e);
    ws.addEventListener('message', cbMessage);
    const cbClose = e => refWsClose.current(e);
    ws.addEventListener('close', cbClose);
    return () => {
      ws.removeEventListener('message', cbMessage);
      ws.removeEventListener('close', cbClose);
    }
  }, [ws, refWsMessage, refWsClose ]);

  useEffect(() => {
    console.log(`media-control - WebSocket or Stream changed`);

    const join = () => {
      sendMessage({ 
        type: 'join',
        config: {
          hasAudio: stream.audio,
          hasVideo: stream.video
        }
      });
    }
    
    if (ws && stream) {
      console.log(`media-conterol - issuing join request`);
      for (let peer in peers) {
        if (peers[peer].local && peers[peer].dead) {
          /* Allocate a new Object so <MediaControl> will trigger */
          peers[peer] = Object.assign({}, peers[peer]);
          delete peers[peer].dead;
          setPeers(Object.assign({}, peers));
        }
      }
      join();
    }
  }, [ws, stream, peers, setPeers, sendMessage]);
 
  useEffect(() => {
    if (!name) {
      return;
    }
    let update = false;
    if (stream) {
      if (!(name in peers)) {
        update = true;
        peers[name] = {
          name: name,
          local: true,
          muted: true,
          videoOn: false,
          hasVideo: stream.video,
          hasAudio: stream.audio,
          attributes: {
            local: true,
            srcObject: stream.media
          }
        };
      }
    }

    /* Renaming the local connection requires the peer to be deleted
     * and re-established with the signaling server */
    for (let key in peers) {
      if (peers[key].local && key !== name) {
        delete peers[key];
        update = true;
      }
    }

    if (update) {
      if (debug) console.log(`media-agent - Setting global peers`, peers);
      setPeers(Object.assign({}, peers));
    }
  }, [peers, name, setPeers, stream]);
  
  useEffect(() => {
    if (!ws || !name) {
      return;
    }

    const setup_local_media = () => { 
      /* Ask user for permission to use the computers microphone and/or camera, 
        * attach it to an <audio> or <video> tag if they give us access. */
      console.log(`media-agent - Requesting access to local ` +
        `audio / video inputs`);
  
      /* See Dummy Tracks for more ideas...
       * https://blog.mozilla.org/webrtc/warm-up-with-replacetrack/
       */
      navigator.getUserMedia = (navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.msGetUserMedia);
  
      return navigator.mediaDevices
      .getUserMedia({audio: true, video: true})
      .then((media) => {
        return {
          media: media,
          audio: true,
          video: true
        };
      })
      .catch((error) => {
        console.log(`media-agent - Access to audio and video ` +
          `failed. Trying just audio.`);
        return navigator.mediaDevices
        .getUserMedia({ audio: true, video: false })
        .then((media) => {
          return {
            media: media,
            audio: true,
            video: false
          };
        })
        .catch((error) => {
          console.log(`media-agent - Access to audio ` +
            `failed.`);
          return {
            media: undefined,
            audio: false,
            video: false
          };
        });
      })
      .then((context) => { /* user accepted access to a/v */
        sendMessage({type: 'media-status',
          video: context.video,
          audio: context.audio
        });
        
        if (context.video) {
          console.log("media-agent - Access granted to audio/video");
          context.media.getVideoTracks().forEach((track) => {
            track.applyConstraints({
              "video": {
                "width": {
                  "min": 160,
                  "max": 320
                },
                "height": {
                  "min": 120,
                  "max": 240
                } 
              }
            });
          });
          return context;
        }

        const black = ({ width = 640, height = 480 } = {}) => {
          const canvas = Object.assign(document.createElement("canvas"), {
            width, height
          });
          canvas.getContext('2d').fillRect(0, 0, width, height);
          const stream = canvas.captureStream();
          return Object.assign(stream.getVideoTracks()[0], {
            enabled: true
          });
        }

        const silence = () => {
          const ctx = new AudioContext(), oscillator = ctx.createOscillator();
          const dst = oscillator.connect(ctx.createMediaStreamDestination());
          oscillator.start();
          return Object.assign(dst.stream.getAudioTracks()[0], {
            enabled: true
          });
        }

        if (context.audio) {
          console.log("media-agent - Access granted to audio");

          let black = ({ width = 640, height = 480 } = {}) => {
            let canvas = Object.assign(document.createElement("canvas"), {
              width, height
            });
            canvas.getContext('2d').fillRect(0, 0, width, height);
            let stream = canvas.captureStream();
            return Object.assign(stream.getVideoTracks()[0], {
              enabled: true
            });
          }
          context.media = new MediaStream([context.media, black()]);
          return context;
        }

        context.media = new MediaStream([black(), silence()]);
        return context;
      });
    };
  
    let abort = false;
    if (!stream) {
      if (debug) console.log(`media-agent - WebSocket open request. ` +
        `Attempting to create local media.`);
      setup_local_media().then((context) => {
        /* once the user has given us access to their
         * microphone/camcorder, join the channel and start peering up */
        if (abort) {
          console.log(`media-agent - aborting setting local media`);
        } else {
          setStream(context);
        }
      }).catch((error) => { /* user denied access to a/v */
        console.error(error);
        console.log("media-agent - Access denied for audio/video");
      });
    }

    return () => {
      abort = true;
      if (!stream) {
        console.log(`media-agent - abort media setup!`);
      }
    };
  }, [ws, setStream, stream, name, sendMessage]);
 
  return <></>;
}

const MediaControl = ({isSelf, peer, className}) => {
  const [media, setMedia] = useState(undefined);
  const [muted, setMuted] = useState(undefined);
  const [videoOn, setVideoOn] = useState(undefined);
  const [target, setTarget] = useState();
  const [frame, setFrame] = useState({
    translate: [0, 0],
  });

  useEffect(() => {
    if (peer && peer.name) {
      setTarget(document.querySelector(
        `.MediaControl[data-peer="${peer.name}"]`));
    }
  }, [setTarget, peer]);

  /* local state is used to trigger re-renders, and the global
   * state is kept up to date in the peers object so re-assignment
   * of sessions doesn't kill the peer or change the mute/video states */
  useEffect(() => {
    if (!peer) {
      setMedia(undefined);
      return;
    }
    setMuted(peer.muted);
    setVideoOn(peer.videoOn);
    setMedia(peer);
  }, [peer, setMedia, setMuted, setVideoOn]);

  console.log(`media-control - render`);

  const toggleMute = (event) => {
    if (debug) console.log(`media-control - toggleMute - ${peer.name}`, 
      !muted);
    peer.muted = !muted;
    setMuted(peer.muted);
    event.stopPropagation();
  }
  
  const toggleVideo = (event) => {
    if (debug) console.log(`media-control - toggleVideo - ${peer.name}`, 
      !videoOn);
    peer.videoOn = !videoOn;
    if (peer.videoOn) {
      const video = document.querySelector(`video[data-id="${media.name}"`);
      if (video) {
        video.play();
      }
    }
    setVideoOn(peer.videoOn);
    event.stopPropagation();
  }

  useEffect(() => {
    if (!media || media.dead || !peer) {
      return;
    }
    if (media.attributes.srcObject) {
      console.log(`media-control - audio enable - ${peer.name}:${!muted}`);
      media.attributes.srcObject.getAudioTracks().forEach((track) => {
        track.enabled = media.hasAudio && !muted;
      });
    }
  }); /* run after every render to hit when ontrack has received and set
       * the stream //,  [media, muted]); */

  useEffect(() => {
    if (!media || media.dead || !peer) {
      return;
    }
    if (media.attributes.srcObject) {
      console.log(`media-control - video enable - ${peer.name}:${videoOn}`);
      media.attributes.srcObject.getVideoTracks().forEach((track) => {
        track.enabled = media.hasVideo && videoOn;
      });
    }
  }); /* run after every render  to hit when ontrack has received and set
       * the stream //, [media, videoOn]); */

  const isValid = media && !media.dead,
    colorAudio = (isValid && media.hasAudio) ? 'primary' : 'disabled',
    colorVideo = (isValid && media.hasVideo) ? 'primary' : 'disabled';

  if (!peer) {
    return <></>;
  }

  return <>
    <div className={`MediaControlSpacer ${className}`}/>
    <div className={`MediaControl ${className}`} data-peer={peer.name}>
      <div className="Controls" >
      { isSelf && <div onTouchStart={toggleMute} onClick={toggleMute}>
          { muted && <MicOff color={colorAudio}/> }
          {!muted && <Mic color={colorAudio}/> }
        </div> }
        { !isSelf && <div onTouchStart={toggleMute} onClick={toggleMute}>
          {muted && <VolumeOff color={colorAudio}/> }
          {!muted && <VolumeUp color={colorAudio}/> }
        </div> }
        <div onTouchStart={toggleVideo} onClick={toggleVideo}>
          { !videoOn && <VideocamOff color={colorVideo}/> }
          {videoOn && <Videocam color={colorVideo}/> }
        </div>
      </div>
      { isValid && <>
        <Moveable
          pinchable={true}
          draggable={true}
          target={target}
          resizable={true}
          keepRatio={true}
          throttleResize={0}
          hideDefaultLines={false}
          edge={true}
          zoom={1}
          origin={false}
          onDragStart={e => {
            e.set(frame.translate);
          }}
          onDrag={e => {
            frame.translate = e.beforeTranslate;
          }}
          onResizeStart={e => {
            e.setOrigin(["%", "%"]);
            e.dragStart && e.dragStart.set(frame.translate);
          }}
          onResize={e => {
            const { translate, rotate, transformOrigin } = frame;
            e.target.style.width = `${e.width}px`;
            e.target.style.height = `${e.height}px`;
            e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
          }}
          onRender={e => {
            const { translate, rotate, transformOrigin } = frame;
            //e.target.style.transformOrigin = transformOrigin;
            e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`; 
          }}
        /><Video className="Video"
          data-id={media.name}
          autoPlay='autoplay'
          {...media.attributes}/>
      </> }
      { !isValid && <video className="Video"></video> }
    </div>
  </>;
};

export { MediaControl, MediaAgent };
