
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import { useApi } from './useApi';
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import {
  BrowserRouter as Router,
  Route,
  Routes,
  useParams
} from "react-router-dom";
import equal from "fast-deep-equal";
import './App.css';

const base = process.env.PUBLIC_URL; /* /identities -- set in .env */

const makeFaceBoxes = (photo: any, dimensions: any, onFaceClick: any): any => {
  const faces: FaceData[] = photo.faces;

  let width: number, height: number, offsetLeft = 0, offsetTop = 0;

  /* If photo is wider than viewport, it will be 100% width and < 100% height */
  if (photo.width / photo.height > dimensions.width / dimensions.height) {
    width = dimensions.width;
    height = dimensions.height * photo.height / photo.width * 
      dimensions.width / dimensions.height;
    offsetLeft = 0;
    offsetTop = (dimensions.height - height) * 0.5;
  } else {
    width = dimensions.width * photo.width / photo.height * 
      dimensions.height / dimensions.width;
    height = dimensions.height;
    offsetLeft = (dimensions.width - width) * 0.5;
    offsetTop = 0;
  }

  return faces.map((face: FaceData) => {
    const faceId = face.faceId;
    const identityId = face.identityId;
    return (
      <div className="FaceBox"
        key={faceId}
        data-identity-id={identityId}
        data-face-id={faceId}
        style={{
          left: offsetLeft + Math.floor(face.left * width) + "px",
          top: offsetTop + Math.floor(face.top * height) + "px",
          width: Math.floor((face.right - face.left) * width) + "px",
          height: Math.floor((face.bottom - face.top) * height) + "px"
        }}
        onClick={(e) => { onFaceClick(e, face) }}
        onMouseEnter={(e) => { onFaceMouseEnter(e, face) }}
        onMouseLeave={(e) => { onFaceMouseLeave(e, face) }}
      />
    )
  });
};

const Photo = ({ photoId, onFaceClick }: any) => {
  const [image, setImage] = useState<any>(undefined);
  const ref = useRef(null);
  const [dimensions, setDimensions] = React.useState({width: 0, height: 0});

  const faces = useMemo(() => {
    if (image === undefined || dimensions.height === 0) {
      return <></>;
    }
    return makeFaceBoxes(image, dimensions, onFaceClick);
  }, [image, dimensions, onFaceClick]);

  const checkResize = useCallback(() => {
    if (!ref.current) {
      return;
    }

    const el: Element = ref.current as Element;
    if (dimensions.height !== el.clientHeight
      || dimensions.width !== el.clientWidth) {
      setDimensions({
        height: el.clientHeight,
        width: el.clientWidth
      })
    }
  }, [setDimensions, dimensions]);

 
  useEffect(() => {
    let timer = setInterval(() => checkResize(), 250);
    return () => { clearInterval(timer); }
  }, [checkResize]);

  useEffect(() => {
    if (photoId === 0) {
      return;
    }
    const fetchImageData = async (image: number) => {
      console.log(`Loading photo ${image}`);
      const res = await window.fetch(`${base}/api/v1/photos/${image}`);
      const photo = await res.json();
      setImage(photo);
    };

    fetchImageData(photoId);
  }, [photoId, setImage]);

  if (image === undefined) {
    return <></>
  }

  return (<div className="Image" ref={ref}>
    <img 
      src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}
      style={{
        objectFit: 'contain',
        width: '100%',
        height: '100%'
      }} />
      { faces }
    </div>
  );
};

const onFaceMouseEnter = (e: any, face: FaceData) => {
  const faceId = face.faceId;
  const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)];

  if (face.identity) {
    const identityId = face.identity.identityId;
    els.splice(0, 0, 
      ...document.querySelectorAll(
        `.Identities [data-identity-id="${identityId}"]`),
      ...document.querySelectorAll(
        `.Photo [data-identity-id="${identityId}"]`));
  }

  els.forEach(el => {
    el.classList.add('Active');
  })
};

const onFaceMouseLeave = (e: any, face: FaceData) => {
  const faceId = face.faceId;
  const els = [...document.querySelectorAll(`[data-face-id="${faceId}"]`)];

  if (face.identity) {
    const identityId = face.identity.identityId;
    els.splice(0, 0,
      ...document.querySelectorAll(`[data-identity-id="${identityId}"]`));
  }

  els.forEach(el => {
    el.classList.remove('Active');
  })
};

const Face = ({ face, onFaceClick, title, ...rest }: any) => {
  const faceId = face.faceId;
  const idPath = String(faceId % 100).padStart(2, '0');
  return (
    <div 
      data-face-id={face.faceId}
      data-identity-id={face.identityId}
      {...rest} 
      onClick={(e) => { onFaceClick(e, face) }}
      onMouseEnter={(e) => { onFaceMouseEnter(e, face) }}
      onMouseLeave={(e) => { onFaceMouseLeave(e, face) }}
      className='Face'>
      <div className='Image'>
        <img src={`${base}/../faces/${idPath}/${faceId}.jpg`}
          style={{
            objectFit: 'contain',
            width: '100%',
            height: '100%'
          }}/>
        <div className='Title'>{title}</div>
      </div>
    </div>
  );
};

type ClusterProps = {
  identity: IdentityData,
  setImage(image: number): void,
  setSelected(selected: number[]): void,
  setIdentity(identity: IdentityData): void
};

const Cluster = ({ identity, setIdentity, setImage, setSelected }: ClusterProps) => {
  console.log(identity);

  const relatedFacesJSX = useMemo(() => {
    const faceClicked = async (e: any, face: FaceData) => {
      if (!identity) {
        return;
      }
      const el = e.currentTarget;
    
      /* Control -- select / deselect single item */
      if (e.ctrlKey) {
        const cluster = document.querySelector('.Cluster');
        el.classList.toggle('Selected');
        if (!cluster) {
          return;
        }

        const selected = [...cluster.querySelectorAll('.Selected')]
          .map((face: any) => face.getAttribute('data-face-id'));
        setSelected(selected);
        return;
      } 
      
      /* Shift -- select groups */
      if (e.shiftKey) {
        return;
      }

      /* Default to load image */
      e.stopPropagation();
      e.preventDefault();
      setImage(face.photoId);
    }
    if (identity === undefined) {
      return <></>;
    }
    return identity.relatedFaces.map((face: FaceData) => {
      return (
        <div 
          key={face.faceId}
          style={{
            display: "flex", 
            justifyContent: 'center',
            alignItems: 'center'}}>
          <Face
            face={face}
            onFaceClick={faceClicked}
            title={face.distance}/>
        </div>
      );
    });
  }, [identity, setImage, setSelected]);

  const lastNameChanged = (e: any) => {
    setIdentity({...identity, lastName: e.currentTarget.value }); 
  };
  const firstNameChanged = (e: any) => {
    setIdentity({...identity, firstName: e.currentTarget.value });
  };
  const middleNameChanged = (e: any) => {
    setIdentity({...identity, middleName: e.currentTarget.value });
  };
  const displayNameChanged = (e: any) => {
    setIdentity({...identity, displayName: e.currentTarget.value });
  };

  const updateIdentity = async () => {
    try {
      const validFields = [
        'id', 'displayName', 'firstName', 'lastName', 'middleName'];
      const filtered: any = Object.assign({}, identity);
      for (let key in filtered) {
        if (validFields.indexOf(key) == -1) {
          delete filtered[key]
        }
      }
      const res = await window.fetch(
        `${base}/api/v1/identities/${identity.identityId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(filtered)
      });
      const data = await res.json();
      setIdentity({ ...identity });
    } catch (error) {
      console.error(error);
    }
  };

  if (identity === undefined) {
    return (<div className='Cluster'>
      Select identity to load.
    </div>);
  }

  return (
    <div className='Cluster'>
      <div className="Info">
        <form className="IdentityForm">
          <div>Last name:</div>
          <input type="text" 
            value={identity.lastName}    
            onChange={lastNameChanged}/>
          <div>First name:</div>
          <input type="text" 
            value={identity.firstName}
            onChange={firstNameChanged} />
          <div>Middle name:</div><input type="text" 
            value={identity.middleName} 
            onChange={middleNameChanged} />
          <div>Display name:</div>
          <input type="text" 
            value={identity.displayName} 
            onChange={displayNameChanged} />
        </form>
        <Button onClick={updateIdentity}>Update</Button> 
      </div>
      <div>Faces: {identity.relatedFaces.length}</div>
      <div className="Faces">
        { relatedFacesJSX }
      </div>
    </div>
  );
};

type FaceData = {
  faceId: number,
  photoId: number,
  lastName: string,
  firstName: string,
  middleName: string,
  displayName: string,
  identity: IdentityData,
  identityId: number,
  distance: number,
  descriptors: any[],
  top: number
  right: number,
  bottom: number,
  left: number,
};

type IdentityData = {
  lastName: string,
  middleName: string,
  firstName: string,
  descriptors: number[],
  identityId: number
  displayName: string,
  relatedFaces: FaceData[]
};

interface IdentitiesProps {
  identities: IdentityData[],
  onFaceClick(e: any, face: FaceData): void
};

const Identities = ({ identities, onFaceClick } : IdentitiesProps) => {
  const identitiesJSX = useMemo(() => {
    return identities.map((identity) => {
      const face = identity.relatedFaces[0];
      return (
        <div
          key={face.faceId}
          style={{
            display: "flex",
            justifyContent: 'center',
            alignItems: 'center'
          }}>
          <Face
            face={face}
            onFaceClick={onFaceClick}
            title={identity.displayName}/>
        </div>
      );
    });
  }, [ identities, onFaceClick ]);

  return (
    <div className='Identities'>
      { identitiesJSX }
    </div>
  );
};

const Button = ({ onClick, children }: any) => {
  return (
    <button type="button" onClick={onClick}>
      {children}
    </button>
  );
};

const App = () => {
  const [identities, setIdentities] = useState<IdentityData[]>([]);
  const { identityId, faceId } = useParams();
  const [identity, setIdentity] = useState<IdentityData | undefined>(undefined);
  const [image, setImage] = useState<number>(0);
  const { loading, data } = useApi(
    `${base}/api/v1/identities`
  );
  const [selected, setSelected] = useState<number[]>([]);

  const loadIdentity = async (identityId: number) => {
    try {
      const res = await window.fetch(`${base}/api/v1/identities/${identityId}`);
      const data = await res.json();
      setIdentity(data[0]);
    } catch (error) {
      console.error(error);
    }
  };

  /* If the identity changes, update its entry in the identities list */
  useEffect(() => {
    if (!identity || identities.length === 0) {
      return;
    }
    for (let key in identities) {
      if (identities[key].identityId === identity.identityId) {
        let same = true;
        [ 'displayName', 'firstName', 'lastName', 'middleName' ]
        .forEach((field: string) => {
          same = same && (identities[key] as any)[field] === (identity as any)[field];
        });
        if (!same) {
          identities[key] = {
            ...identity, 
            relatedFaces: identities[key].relatedFaces
          };
          /* relatedFaces is a list of references to identity */
          identity.relatedFaces.forEach(face => {
            face.identity = identity;
          });
          setIdentities([...identities]);
        }
        return;
      }
    }
  }, [identity, setIdentities, identities]);

  useEffect(() => {
    if (identityId !== undefined && !isNaN(+identityId)) {
      loadIdentity(+identityId);
    }

    if (faceId !== undefined && !isNaN(+faceId)) {
      setImage(+faceId);
    }
  // eslint-disable-next-line
  }, []);

  useEffect(() => {
    if (data && data.length) {
      data.forEach((identity: IdentityData) => {
        identity.relatedFaces.forEach(face => {
          face.identity = identity;
        });
      });
      setIdentities(data as IdentityData[]);
    }
  }, [data]);

  const removeFacesFromIdentities = (faceIds: number[]) => {
    if (!identity) {
      return;
    }
    const pre = identity.relatedFaces.length;
    /* Remove all relatedFaces which are part of the set of removed
    * faces */
    identity.relatedFaces = identity.relatedFaces.filter(
      (face: FaceData) => faceIds.indexOf(face.faceId) === -1);
    if (pre !== identity.relatedFaces.length) {
      setIdentity({ ...identity })
    }
  }

  const markSelectedIncorrectIdentity = async () => {
    if (!identity) {
      return;
    }
    try {
      const res = await window.fetch(
        `${base}/api/v1/identities/faces/remove/${identity.identityId}`, {
        method: 'PUT', 
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ faces: selected })
      });
      const data = await res.json();

      removeFacesFromIdentities(data.faces);
    } catch (error) {
      console.error(error);
    }
  };

  const markSelectedNotFace = async () => {
    try {
      const res = await window.fetch(
        `${base}/api/v1/faces`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'not-a-face',
          faces: selected 
        })
      });
      const data = await res.json();
      removeFacesFromIdentities(data);
    } catch (error) {
      console.error(error);
    }
  };

  const onFaceClick = (e: any, face: FaceData) => {
    if (!face.identity) {
      console.log(`Face ${face.faceId} does not have an Identity`);
      return;
    }
    const identityId = face.identity.identityId;
    const faceId = face.faceId;
    console.log(`onFaceClick`, { faceId, identityId});
    const faces = [
      ...document.querySelectorAll(`.Identities [data-identity-id="${identityId}"]`),
      ...document.querySelectorAll(`.Cluster [data-face-id="${faceId}"]`)];
    faces.forEach((el: any) => {
      el.scrollIntoView();
    });
  };

  const identitiesOnFaceClick = (e: any, face: FaceData) => {
    const identityId = face.identity.identityId;
    loadIdentity(identityId);
  } 

  return (
    <div className="App">
      <div className="Worksheet">
        <PanelGroup className="Explorer" 
          autoSaveId="persistence" direction="horizontal">
          <Panel defaultSize={50} className="ClusterEditor">
            {loading && <div style={{ margin: '1rem' }}>Loading...</div>}
            {!loading && identity !== undefined && 
              <Cluster {...{ 
                identity, 
                setIdentity, 
                setImage,  
                setSelected 
              }} />}
            {!loading && identity === undefined && <div className="Cluster">
              Select identity to edit
            </div>}
            <div className="Actions">
              { selected.length !== 0 && <>
                <Button onClick={markSelectedIncorrectIdentity}>Remove</Button> 
                <Button onClick={markSelectedNotFace}>Not a face</Button> 
              </>}
            </div>
          </Panel>
          <PanelResizeHandle className="Resizer"/>
          <Panel>
            {image === 0 && <div style={{ margin: '1rem' }}>Select image to view</div>}
            {image !== 0 && <Photo onFaceClick={onFaceClick} photoId={image}/> }
          </Panel>
          <PanelResizeHandle className="Resizer" />
          <Panel defaultSize={8.5} minSize={8.5} className="IdentitiesList">
            { !loading && <Identities 
              {... { onFaceClick: identitiesOnFaceClick, identities }}/> }
          </Panel>
          </PanelGroup>
      </div>
    </div>
  );
}

const AppRouter = () => {
  return (
    <Router>
      <Routes>
        <Route element={<App />} path={`${base}/:identityId?/:faceId?`} />
      </Routes>
    </Router>
  );
}

export default AppRouter;
