/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */


package com.sun.cldchi.tools.memoryprofiler.data;

import com.sun.cldchi.tools.memoryprofiler.jdwp.VMConnection;
import com.sun.cldchi.tools.memoryprofiler.jdwp.VMReply;
import com.sun.cldchi.tools.memoryprofiler.jdwp.BoundException;
import com.sun.cldchi.tools.memoryprofiler.jdwp.DebugeeException;

import java.net.*;
import java.io.*;
import java.util.*;

class GlobalData implements MPDataProvider {
  private VMConnection _connector;
  private int _heap_start;
  private int _heap_top;
  private int _old_gen_end;
  private int _allocation_top;
  private HashMap _allJavaObjects = new HashMap();
  private HashMap _allClasses = new HashMap();

    // memory profiler command constants
  private static final int MPGetGlobalData     = 0x1201;
  private static final int MPGetHeapData       = 0x1202;
  private static final int MPGetClasses        = 0x1203;
  private static final int MPGetRoots          = 0x1204;
  private static final int MPVMSuspend         = 0x1205;
  private static final int MPVMResume          = 0x1206;
  private static final int MPVMStackTrace      = 0x1207;

  //presentation strings
  public static final String InternalObjectName = "VM Internal object";
  public static final String StaticsObjectName =  "Statics of class ";
  public static final String StackObjectName =    "Stack object";

  private static final int  CLASS_ID_OFFSET = 0;
  private static final int  TASK_ID_OFFSET = 16;
  private static final int  OBJ_TYPE_OFFSET = 23;

  public int get_heap_start() {return _heap_start;};
  public int get_heap_top() {return _heap_top;};
  public int get_old_gen_end() {return _old_gen_end;};
  public int get_allocation_top() {return _allocation_top;};

  GlobalData(VMConnection connection) {
    _connector = connection;
  }

  private void update() throws SocketException {
    getClassList();
    getAllData();
    getRoots();
    try {
      VMReply r = _connector.sendReplyCommand(MPGetGlobalData);
      _heap_start =     r.getInt();
      _heap_top =       r.getInt();
      _old_gen_end =    r.getInt();
      _allocation_top = r.getInt();
    }catch(DebugeeException e) {
      reset();
      throw new SocketException(e.getMessage());
    }catch(BoundException e) {
      reset();
      throw new SocketException(e.getMessage());
    }
    calculateDeadObjects();
  }

  public JavaClass[] getClassList() throws SocketException {
    _allClasses.clear();
    JavaClass[] result = null;
    try {
      VMReply r = _connector.sendReplyCommand(MPGetClasses);
      int classesCount = r.getInt();
      result = new JavaClass[classesCount];
      for(int i=0 ; i<classesCount ; i++){
        int class_id = (int)r.getInt();
        String class_name = objectTypeNameFromJNI(r.getString());
        JavaClass new_item = new JavaClass(class_id, class_name);
        _allClasses.put(new Integer(class_id), new_item);
        result[i] = new_item;
      }
    } catch (Exception e) {
      reset();
      throw new SocketException(e.getMessage());
    }
    return result;
  }

  private void getAllData() throws SocketException {
    int read = 1;
    int oread = 1;
    _allJavaObjects.clear();
    try {
      while (true) {
        VMReply r = _connector.sendReplyCommand(MPGetHeapData);
        int object_address = r.getInt();
        while (object_address != -1 && object_address != -2) {
          int size = r.getInt();
          int mp_class_id = r.getInt();
          int object_type = mp_class_id >> OBJ_TYPE_OFFSET;
          int stack_number = -1;
          if (object_type == STACK_OBJECT) {
            stack_number = r.getInt();
            read++;
          } 
          int links = r.getInt();
          int[] refs = new int[links];
          HashMap offsets = new HashMap(links);
          if (object_type == STACK_OBJECT) {
            for (int i = 0; i < links; i++) {
              refs[i] = r.getInt();
              int offset = r.getInt();
              Integer cur_offset = (Integer)offsets.get(new Integer(refs[i]));
              if (cur_offset == null || cur_offset.intValue() < offset) {
                offsets.put(new Integer(refs[i]), new Integer(offset));
              }
            }
            read += links;
          } else {
            for (int i = 0; i < links; i++) {
              refs[i] = r.getInt();
            }
          }
          mp_class_id = mp_class_id & 0x7FFFFF;
          JavaClass class_item = (JavaClass)_allClasses.get(new Integer(mp_class_id));
          int class_id = mp_class_id;
          if (class_item != null) {
            class_id = class_item.id;
          }
          if (object_type == JAVA_OBJECT) {
            _allJavaObjects.put(new Integer(object_address), 
               new JavaObject(object_address, class_id, size, refs, JAVA_OBJECT));
          } else if (object_type == STATICS_OBJECT) {
            _allJavaObjects.put(new Integer(object_address), 
               new JavaObject(object_address, class_id, size, refs, STATICS_OBJECT));
          } else if (object_type == STACK_OBJECT) {
            _allJavaObjects.put(new Integer(object_address), 
               new JavaObject(object_address, -1, size, refs, STACK_OBJECT, offsets, stack_number));
          } else if (object_type == VM_OBJECT) {
            _allJavaObjects.put(new Integer(object_address), 
               new JavaObject(object_address, -1, size, refs, VM_OBJECT));
          } else {
            System.out.println("Wrong response from VM! Unknown object type. Skipped!");
          }
          object_address = r.getInt();
          read += 4;
          read += links;
          oread++;
        }         
        if (object_address == -1) break;
      }
    } catch (Exception e) {
      reset();
      throw new SocketException(e.getMessage());
    }
    updateAllObjects();
  }

  private void updateAllObjects() {
    for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext();) {
      JavaObject obj = (JavaObject)it.next();
      for (int i = 0; i < obj._references_addresses.length; i++) {
        JavaObject ref = getObjectByAddress(obj._references_addresses[i]);
        if (ref != null) {
          obj.add_reference(ref);
          ref.add_referee(obj);
        } 
      }
    }
  }

  private JavaObject getObjectByAddress(int address) {
    Integer key = new Integer(address);
    return (JavaObject)_allJavaObjects.get(key);
  }

  public JavaObject[] getObjectsOfClass(JavaClass jc) {
    if (jc == null) return new JavaObject[0];
    int obj_count = 0;
    int class_id = jc.id;

    for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext(); ) {
      JavaObject elem = (JavaObject)it.next();
      if (elem.object_type == JAVA_OBJECT && elem.class_id == class_id)
        obj_count++;
    } 
    JavaObject result[] = new JavaObject[obj_count];
    obj_count = 0;
    for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext(); ) {
      JavaObject elem = (JavaObject)it.next();
      if (elem.object_type == JAVA_OBJECT && elem.class_id == class_id)
        result[obj_count++] = elem;
    }
    return result;
  }

  public Iterator getObjects() {
    return _allJavaObjects.values().iterator();
  }
  public void connect(String hostName, int port) throws java.net.ConnectException, SocketException  {
    _connector.connect(hostName, port);    
    update();
  }

  public String getObjectTypeName(JavaObject obj) {
    if (obj.object_type == JAVA_OBJECT || obj.object_type == STATICS_OBJECT) {
      JavaClass item = (JavaClass)_allClasses.get(new Integer(obj.class_id)); 
      if (item == null) { //this is just for debug!
        System.out.println(obj.class_id);
        System.out.println(obj.object_type);
        return "null!";
      }
      if (obj.object_type == JAVA_OBJECT) {
        return item.name;
      } else { //statics object
        return StaticsObjectName + item.name;
      }    
    } else if (obj.object_type == STACK_OBJECT) {
      return StackObjectName;
    } else if (obj.object_type == VM_OBJECT) {        
      return InternalObjectName;
    } else {
      return "Wrong object type! Report a bug please!";
    }
  }

  private void getRoots() throws SocketException {
    try {
      VMReply r = _connector.sendReplyCommand(MPGetRoots);
      int root = r.getInt();
      while (root != -1) {
        Integer key = new Integer(root);
        JavaObject obj = (JavaObject)_allJavaObjects.get(key);
        if (obj != null) {
          obj.setRootDistance(0);
        }
        root = r.getInt();
      }
    } catch (Exception e) {
      reset();
      throw new SocketException(e.getMessage());
    }

  }

  private void calculateDeadObjects() {
    boolean was_updated = true;
    while (was_updated) {
      was_updated = false;
      for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext(); ) {
        JavaObject elem = (JavaObject)it.next();
        if (elem.getRootDistance() == -1)
          continue;
        Object[] refs = elem.get_references();
        for (int i = 0; i < refs.length; i++) {
          JavaObject obj = (JavaObject)refs[i];
          if (obj.getRootDistance() == -1) {
            obj.setRootDistance(elem.getRootDistance() + 1);
            was_updated = true;
          } else if (obj.getRootDistance() > 1 + elem.getRootDistance()) {
            obj.setRootDistance(elem.getRootDistance() + 1);
            was_updated = true;
          }
        }
      } 
    }   
  }

  public JavaObject[] pathFromTheRoot(JavaObject obj) {
    if (obj == null) return null;
    if (obj.getRootDistance() == -1) return null;
    JavaObject[] result = new JavaObject[obj.getRootDistance() + 1];
    int dst = obj.getRootDistance();
    while (dst > 0) { 
      result[dst--] = obj;
      Object[] referees = obj.get_referees();
      for (int i = 0; i < referees.length; i++) {
        JavaObject elem = (JavaObject)referees[i];
        if (elem.getRootDistance() == obj.getRootDistance() - 1) {
          obj = elem; break;
        }
      }      
      if (dst != obj.getRootDistance()) throw new RuntimeException("problem here!");      
    }
    result[dst--] = obj;
    return result;
  } 

  public JavaObject[] getObjectsFromTheAddresses(int start, int end) {
    if (start > end) throw new RuntimeException();
    int obj_count = 0;
    for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext(); ) {
      JavaObject obj = (JavaObject)it.next();
      if (obj.address + obj.size > start && obj.address < end) {
        obj_count++;
      }
    }
    JavaObject[] result = new JavaObject[obj_count];
    obj_count = 0;
    for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext(); ) {
      JavaObject obj = (JavaObject)it.next();
      if (obj.address + obj.size > start && obj.address < end) {
        result[obj_count++] = obj;
      }
    }
    Arrays.sort(result, new Comparator() {
     public int	compare(Object o1, Object o2) {
       return ((JavaObject)o1).address - ((JavaObject)o2).address;
     } 
    });
    return result;
  } 

  public void pauseVM() throws SocketException { 
    try {
      _connector.sendCommand(MPVMSuspend);
      update();
    } catch (Exception e ) {
      reset();
      throw new SocketException(e.getMessage());
    }
  } 

  public void resumeVM() throws SocketException { 
    try {
      reset();
      _connector.sendCommand(MPVMResume);
    } catch (Exception e ) {
      reset();
      throw new SocketException(e.getMessage());
    }
  } 

  public ClassStatistics[] calculateStatistics() {

    ClassStatistics.reset();
    int count = _allClasses.values().size() + 1;
    HashMap result = new HashMap(count);
    for (Iterator it = _allClasses.values().iterator(); it.hasNext();) {
      JavaClass item = (JavaClass)it.next();
      result.put(new Integer(item.id), new ClassStatistics(item.name));
    }
    result.put(new Integer(-1), new ClassStatistics("Internal VM Objects"));
    for (Iterator it = _allJavaObjects.values().iterator(); it.hasNext();) {
      JavaObject obj = (JavaObject)it.next();
      int type_id = obj.class_id;      
      if (obj.object_type != JAVA_OBJECT) {
         type_id = -1;
      }
      ClassStatistics cls = (ClassStatistics)result.get(new Integer(type_id));
      if (cls == null) continue; //shall not happend
      cls.add(obj, get_old_gen_end());
    }
    Object[] arr = result.values().toArray();
    Arrays.sort(arr, new Comparator() {
      public int compare(Object obj1, Object obj2) {
        ClassStatistics cls1 = (ClassStatistics)obj1;
        ClassStatistics cls2 = (ClassStatistics)obj2;
        return cls2.getHeapPercentage() - cls1.getHeapPercentage();
      }  
    });
    ClassStatistics[] res = new ClassStatistics[arr.length];
    for (int i = 0; i < res.length; i++) {
      res[i] = (ClassStatistics)arr[i];
    }

    return res;    
  }

  /**
   * Converts object type name from JNI form to <code>javap</code>-like
   * form (for example, <code>Ljava/lang/String;</code> will be converted to
   * <code>java.lang.String</code>). This method is
   * used by <code>fields</code> and <code>methods</code> KJDB commands.
   *
   * @param jniTypeName an object type name in JNI form
   * @return an object type name in <code>javap</code>-like form
   */
  private String objectTypeNameFromJNI(String jniTypeName){
    if(jniTypeName.indexOf("L") != 0 ||
      jniTypeName.lastIndexOf(";") != jniTypeName.length() - 1){
        return jniTypeName;
    }
    char[] chars = new char[jniTypeName.length()-2];
    jniTypeName.getChars(1,jniTypeName.length()-1,chars,0);
    for(int i=0 ; i<chars.length ; i++){
      if(chars[i] == '/'){
        chars[i] = '.';
      }
    }
    return new String(chars);
  }

  public void closeConnections() {
     reset();
    _connector.closeConnections();
  }

  private void setConnector(VMConnection connector) {
    if (_connector == null ) {
      _connector = connector;
      return;
    }
    if (_connector.isConnected()) {
      throw new IllegalStateException("Another connector is connected to the VM");
    }
    _connector = connector;
    reset();
  }

  public String getStackTrace(JavaObject stack_object, int ptr) throws SocketException {
    if (stack_object.object_type != STACK_OBJECT) {
      return "JavaObject of wrong type type was passed to MPDataProvider.getStackTrace.\n" +
                   "Please report a bug!";
    }
    int[] params = new int[2];    
    params[0] = stack_object._stack_id;
    Integer offset = (Integer)stack_object._stack_offsets.get(new Integer(ptr));
    params[1] = offset.intValue();
    String result = null;
    try {
      VMReply reply = _connector.sendReplyCommand(MPVMStackTrace, params);
      result = reply.getString();
    } catch (Exception e) {
      result = "VM connection is broken!";
      reset();
      throw new SocketException(e.getMessage());
    }
    return result;
  }

  private void reset() {
    _heap_start = 0;
    _heap_top = 0;
    _old_gen_end = 0;
    _allocation_top = 0;
    _allJavaObjects.clear();
    _allClasses.clear();
    ClassStatistics.reset();
  }
}
