/**
   * Gets the van der Waals radius of the given atom following the values defined by Chothia (1976)
   * J.Mol.Biol.105,1-14 NOTE: the vdw values defined by the paper assume no Hydrogens and thus
   * "inflates" slightly the heavy atoms to account for Hydrogens. Thus this method cannot be used
   * in a structure that contains Hydrogens!
   *
   * <p>If atom is neither part of a nucleotide nor of a standard aminoacid, the default vdw radius
   * for the element is returned. If atom is of unknown type (element) the vdw radius of {@link
   * #Element().N} is returned
   *
   * @param atom
   * @return
   */
  public static double getRadius(Atom atom) {

    if (atom.getElement() == null) {
      System.err.println(
          "Warning: unrecognised atom "
              + atom.getName()
              + " with serial "
              + atom.getPDBserial()
              + ", assigning the default vdw radius (Nitrogen vdw radius).");
      return Element.N.getVDWRadius();
    }

    Group res = atom.getGroup();

    if (res == null) {
      System.err.println(
          "Warning: unknown parent residue for atom "
              + atom.getName()
              + " with serial "
              + atom.getPDBserial()
              + ", assigning its default vdw radius");
      return atom.getElement().getVDWRadius();
    }

    String type = res.getType();

    if (type.equals(GroupType.AMINOACID)) return getRadiusForAmino(((AminoAcid) res), atom);

    if (type.equals(GroupType.NUCLEOTIDE)) return getRadiusForNucl((NucleotideImpl) res, atom);

    return atom.getElement().getVDWRadius();
  }
  /**
   * Constructs a new AsaCalculator. Subsequently call {@link #calculateAsas()} or {@link
   * #getGroupAsas()} to calculate the ASAs.
   *
   * @param atoms an array of atoms not containing Hydrogen atoms
   * @param probe the probe size
   * @param nSpherePoints the number of points to be used in generating the spherical dot-density,
   *     the more points the more accurate (and slower) calculation
   * @param nThreads the number of parallel threads to use for the calculation
   * @throws IllegalArgumentException if any atom in the array is a Hydrogen atom
   */
  public AsaCalculator(Atom[] atoms, double probe, int nSpherePoints, int nThreads) {
    this.atoms = atoms;
    this.probe = probe;
    this.nThreads = nThreads;

    for (Atom atom : atoms) {
      if (atom.getElement() == Element.H)
        throw new IllegalArgumentException(
            "Can't calculate ASA for an array that contains Hydrogen atoms ");
    }

    // initialising the radii by looking them up through AtomRadii
    radii = new double[atoms.length];
    for (int i = 0; i < atoms.length; i++) {
      radii[i] = getRadius(atoms[i]);
    }

    // initialising the sphere points to sample
    spherePoints = generateSpherePoints(nSpherePoints);

    cons = 4.0 * Math.PI / (double) nSpherePoints;
  }
  /**
   * Gets the radius for given nucleotide atom
   *
   * @param atom
   * @return
   */
  private static double getRadiusForNucl(NucleotideImpl nuc, Atom atom) {

    if (atom.getElement().equals(Element.H)) return Element.H.getVDWRadius();
    if (atom.getElement().equals(Element.D)) return Element.D.getVDWRadius();

    if (atom.getElement() == Element.C) return NUC_CARBON_VDW;

    if (atom.getElement() == Element.N) return NUC_NITROGEN_VDW;

    if (atom.getElement() == Element.P) return PHOSPHOROUS_VDW;

    if (atom.getElement() == Element.O) return OXIGEN_VDW;

    System.err.println(
        "Warning: unexpected atom "
            + atom.getName()
            + " for nucleotide "
            + nuc.getPDBName()
            + ", assigning its standard vdw radius");
    return atom.getElement().getVDWRadius();
  }
  private double calcSingleAsa(int i) {
    Atom atom_i = atoms[i];
    ArrayList<Integer> neighbor_indices = findNeighborIndices(i);
    int n_neighbor = neighbor_indices.size();
    int j_closest_neighbor = 0;
    double radius = probe + radii[i];

    int n_accessible_point = 0;

    for (Point3d point : spherePoints) {
      boolean is_accessible = true;
      Point3d test_point =
          new Point3d(
              point.x * radius + atom_i.getX(),
              point.y * radius + atom_i.getY(),
              point.z * radius + atom_i.getZ());

      int[] cycled_indices = new int[n_neighbor];
      int arind = 0;
      for (int ind = j_closest_neighbor; ind < n_neighbor; ind++) {
        cycled_indices[arind] = ind;
        arind++;
      }
      for (int ind = 0; ind < j_closest_neighbor; ind++) {
        cycled_indices[arind] = ind;
        arind++;
      }

      for (int j : cycled_indices) {
        Atom atom_j = atoms[neighbor_indices.get(j)];
        double r = radii[neighbor_indices.get(j)] + probe;
        double diff_sq = test_point.distanceSquared(new Point3d(atom_j.getCoords()));
        if (diff_sq < r * r) {
          j_closest_neighbor = j;
          is_accessible = false;
          break;
        }
      }
      if (is_accessible) {
        n_accessible_point++;
      }
    }
    return cons * n_accessible_point * radius * radius;
  }
  /**
   * Gets the radius for given amino acid and atom
   *
   * @param aa
   * @param atom
   * @return
   */
  private static double getRadiusForAmino(AminoAcid amino, Atom atom) {

    if (atom.getElement().equals(Element.H)) return Element.H.getVDWRadius();
    // some unusual entries (e.g. 1tes) contain Deuterium atoms in standard aminoacids
    if (atom.getElement().equals(Element.D)) return Element.D.getVDWRadius();

    String atomCode = atom.getName();
    char aa = amino.getAminoType();

    // here we use the values that Chothia gives in his paper (as NACCESS does)
    if (atom.getElement() == Element.O) {
      return OXIGEN_VDW;
    } else if (atom.getElement() == Element.S) {
      return SULFUR_VDW;
    } else if (atom.getElement() == Element.N) {
      if (atomCode.equals("NZ")) return TETRAHEDRAL_NITROGEN_VDW; // tetrahedral Nitrogen
      return TRIGONAL_NITROGEN_VDW; // trigonal Nitrogen
    } else if (atom.getElement() == Element.C) { // it must be a carbon
      if (atomCode.equals("C")
          || atomCode.equals("CE1")
          || atomCode.equals("CE2")
          || atomCode.equals("CE3")
          || atomCode.equals("CH2")
          || atomCode.equals("CZ")
          || atomCode.equals("CZ2")
          || atomCode.equals("CZ3")) {
        return TRIGONAL_CARBON_VDW; // trigonal Carbon
      } else if (atomCode.equals("CA")
          || atomCode.equals("CB")
          || atomCode.equals("CE")
          || atomCode.equals("CG1")
          || atomCode.equals("CG2")) {
        return TETRAHEDRAL_CARBON_VDW; // tetrahedral Carbon
      }
      // the rest of the cases (CD, CD1, CD2, CG) depend on amino acid
      else {
        switch (aa) {
          case 'F':
          case 'W':
          case 'Y':
          case 'H':
          case 'D':
          case 'N':
            return TRIGONAL_CARBON_VDW;

          case 'P':
          case 'K':
          case 'R':
          case 'M':
          case 'I':
          case 'L':
            return TETRAHEDRAL_CARBON_VDW;

          case 'Q':
          case 'E':
            if (atomCode.equals("CD")) return TRIGONAL_CARBON_VDW;
            else if (atomCode.equals("CG")) return TETRAHEDRAL_CARBON_VDW;

          default:
            System.err.println(
                "Warning: unexpected carbon atom "
                    + atomCode
                    + " for aminoacid "
                    + aa
                    + ", assigning its standard vdw radius");
            return Element.C.getVDWRadius();
        }
      }

      // not any of the expected atoms
    } else {
      System.err.println(
          "Warning: unexpected atom "
              + atomCode
              + " for aminoacid "
              + aa
              + ", assigning its standard vdw radius");
      return atom.getElement().getVDWRadius();
    }
  }