about summary refs log blame commit diff stats
path: root/drawing_tests.lua
blob: bc3b654befb796e2dcfd75f17b822b5a482d8484 (plain) (tree)
1
2
3
4
5
6
7
8
9



                                                                              


                                           

                                   
             
                                      
                                                           





                                                                           



                                                                                         


                                                                                                      



                                                                                                          
             




                                                                                                                              
                


                                                                                                                          

                                                            
                                                                        





                                                 


                         

                                                                              

                                                            








                                                                             
   



                                                                                                      


                                                                                                          
             




                                                                                                                                         
                                                      


                                                                                                                          

                                                                       











                                                                                                      


                                                                                                          
             




                                                                                                                                
                  
                                                                                                                             
                               


                                                                                                                             

                                                              






                                                                                        


                                                                                                      



                                                                                                          
             




                                                                                                                                  
                         
                                                                                                                      

                                  

                                                                                                                          


                                                                


                                                                                                      


                                                                                                          
             
                                                           
                                                                                                                             

                              
                                                                                                                     





                                                                                                      


                                                                                                          
             




                                                                                                                                           
                  

                                                                                                                             
                             

                                                                                                                             

                                                                         









                                                                                                      


                                                                                                          
             




                                                                                                                             
                

                                                                                                                        
                                          

                                                                                                                                         
                                                           
                                                           








                                                                                     



                                                                                                      

                                                                                                          
             





                                                                                                                                 
                
                                                                                                                      

                                              
                                                                                                          

                                           

                                                                                                                          


















                                                                                                      

                                                                                                          
             





                                                                                                                                   
                
                                                                                                                        

                                                
                                                                                                          

                                     
                                                                                                          

                                                                           

                                                                                                                          





















                                                                                                          

                                                                                                          
             





                                                                                                                                                
                
                                                                                                                        

                                                
                                                                                                          

                                     
                                                                                                          
                             
                                       















                                                                                                                       

                                                                                                          
             





                                                                                                                                
                
                                                                                                                        

                                             
                                                                                                          

                                     
                                                                                                          

                                                                 

                                                                                                                          
















                                                                                                       



                                 



                                                                                                          
             
                


                                                                                                                          











                                                                                  
                                                                                   


                                               
                                                                                   

                                  
                                                                                   
                                               


                         
                    

                                                               
                                                    
   



                                 



                                                                                                          
             


                                                                                                                          








                                                                                  


                         
                          


                                                               

                                                  
             

                                               
                                                                                   



                                                                          
                                                                                                          




                                                 

                                                                                                                        
                                                              


                         
                    

                                                               

                                                  
   
 


                                                 



                                                                                                          
             


                                                                                                                          


                                                                                                         
             

                               
                                                                                                     
               
                                                                                                          




                                                                                                    


                                                         



                                                                                                          
             




                                                                                                                          



                                                                                             
                                                                                                          


                                                                                       


                         
                                                    

                                                                                   




                                                         


                                                                                                          
             




                                                                                                                          



                                                                                                       
                                                                                                          








                                                                                                 


                                                                                                          
             
                
                                                                                                                      

                                              
                                                                                                          

                                           
                                                                                                          

                                           

                                                                                                                          



                                                                                                  
                                                                                                          








                                                                                         


                                                                                                          
             
                
                                                                                                                      

                                              
                                                                                                          

                                           

                                                                                                                          



                                                                                                  
                                                                                                          



                                                                                   



                                    



                                                                                                          
             
                


                                                                                                                          









                                                                                       
                                                                                   




                                                             

                                                                                          

                               
                                       
                                                 
                                                                                 
                                                                                                      


                         
                  

                                                               

                                                        



                                    



                                                                                                          
             


                                                                                                                          











                                                                                       
                                                                                                          




                                                  

                                                                                                                        


                                                           
                                       
                                                 
                                                                                 

                                                  


                         
                  

                                                               


                                                       



                                                         



                                                                                                          
             




                                                                                                                          



                                                                                         
                                                                                                          




                                                                                   
                                       
                                                 
                                                                                 

                                                                                


                         
                  

                                                                               
   
-- major tests for drawings
-- We minimize assumptions about specific pixels, and try to test at the level
-- of specific shapes. In particular, no tests of freehand drawings.

function test_creating_drawing_saves()
  io.write('\ntest_creating_drawing_saves')
  App.screen.init{width=120, height=60}
  Editor_state.filename = 'foo'
  Editor_state.lines = load_array{}
  edit.draw()
  -- click on button to create drawing
  App.run_after_mouse_click(8,Editor_state.margin_top+8, 1)
  -- file not immediately saved
  App.update(0.01)
  check_nil(App.filesystem['foo'], 'F - test_creating_drawing_saves/early')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- filesystem contains drawing and an empty line of text
  check_eq(App.filesystem['foo'], '```lines\n```\n\n', 'F - test_creating_drawing_saves')
end

function test_draw_line()
  io.write('\ntest_draw_line')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  check_eq(#Editor_state.lines, 2, 'F - test_draw_line/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_line/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_line/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_line/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_line/baseline/#shapes')
  -- draw a line
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_line/#shapes')
  check_eq(#drawing.points, 2, 'F - test_draw_line/#points')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/shape:1')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_draw_line/p1:x')
  check_eq(p1.y, 6, 'F - test_draw_line/p1:y')
  check_eq(p2.x, 35, 'F - test_draw_line/p2:x')
  check_eq(p2.y, 36, 'F - test_draw_line/p2:y')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- The format on disk isn't perfectly stable. Table fields can be reordered.
  -- So just reload from disk to verify.
  Editor_state.lines = load_from_disk(Editor_state.filename)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_line/save/#shapes')
  check_eq(#drawing.points, 2, 'F - test_draw_line/save/#points')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/save/shape:1')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_draw_line/save/p1:x')
  check_eq(p1.y, 6, 'F - test_draw_line/save/p1:y')
  check_eq(p2.x, 35, 'F - test_draw_line/save/p2:x')
  check_eq(p2.y, 36, 'F - test_draw_line/save/p2:y')
end

function test_draw_horizontal_line()
  io.write('\ntest_draw_horizontal_line')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'manhattan'
  edit.draw()
  check_eq(#Editor_state.lines, 2, 'F - test_draw_horizontal_line/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_horizontal_line/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_horizontal_line/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_horizontal_line/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_horizontal_line/baseline/#shapes')
  -- draw a line that is more horizontal than vertical
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_horizontal_line/#shapes')
  check_eq(#drawing.points, 2, 'F - test_draw_horizontal_line/#points')
  check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_draw_horizontal_line/shape_mode')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_draw_horizontal_line/p1:x')
  check_eq(p1.y, 6, 'F - test_draw_horizontal_line/p1:y')
  check_eq(p2.x, 35, 'F - test_draw_horizontal_line/p2:x')
  check_eq(p2.y, p1.y, 'F - test_draw_horizontal_line/p2:y')
end

function test_draw_circle()
  io.write('\ntest_draw_circle')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  check_eq(#Editor_state.lines, 2, 'F - test_draw_circle/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_circle/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle/baseline/#shapes')
  -- draw a circle
  App.mouse_move(Editor_state.margin_left+4, Editor_state.margin_top+Editor_state.drawing_padding_top+4)  -- hover on drawing
  App.run_after_keychord('C-o')
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35+30, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_circle/#shapes')
  check_eq(#drawing.points, 1, 'F - test_draw_circle/#points')
  check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')
  check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle/radius')
  local center = drawing.points[drawing.shapes[1].center]
  check_eq(center.x, 35, 'F - test_draw_circle/center:x')
  check_eq(center.y, 36, 'F - test_draw_circle/center:y')
end

function test_cancel_stroke()
  io.write('\ntest_cancel_stroke')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  check_eq(#Editor_state.lines, 2, 'F - test_cancel_stroke/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_cancel_stroke/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_cancel_stroke/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_cancel_stroke/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_cancel_stroke/baseline/#shapes')
  -- start drawing a line
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  -- cancel
  App.run_after_keychord('escape')
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 0, 'F - test_cancel_stroke/#shapes')
end

function test_keys_do_not_affect_shape_when_mouse_up()
  io.write('\ntest_keys_do_not_affect_shape_when_mouse_up')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  -- hover over drawing and press 'o' without holding mouse
  App.mouse_move(Editor_state.margin_left+4, Editor_state.margin_top+Editor_state.drawing_padding_top+4)  -- hover on drawing
  App.run_after_keychord('o')
  -- no change to drawing mode
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_keys_do_not_affect_shape_when_mouse_up/drawing_mode')
  -- no change to text either because we didn't run the textinput event
end

function test_draw_circle_mid_stroke()
  io.write('\ntest_draw_circle_mid_stroke')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  check_eq(#Editor_state.lines, 2, 'F - test_draw_circle_mid_stroke/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle_mid_stroke/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes')
  -- draw a circle
  App.mouse_move(Editor_state.margin_left+4, Editor_state.margin_top+Editor_state.drawing_padding_top+4)  -- hover on drawing
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_keychord('o')
  App.run_after_mouse_release(Editor_state.margin_left+35+30, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes')
  check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points')
  check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')
  check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius')
  local center = drawing.points[drawing.shapes[1].center]
  check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x')
  check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y')
end

function test_draw_arc()
  io.write('\ntest_draw_arc')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'circle'
  edit.draw()
  check_eq(#Editor_state.lines, 2, 'F - test_draw_arc/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_arc/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_arc/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes')
  -- draw an arc
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.mouse_move(Editor_state.margin_left+35+30, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('a')  -- arc mode
  App.run_after_mouse_release(Editor_state.margin_left+35+50, Editor_state.margin_top+Editor_state.drawing_padding_top+36+50, 1)  -- 45°
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes')
  check_eq(#drawing.points, 1, 'F - test_draw_arc/#points')
  check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode')
  local arc = drawing.shapes[1]
  check_eq(arc.radius, 30, 'F - test_draw_arc/radius')
  local center = drawing.points[arc.center]
  check_eq(center.x, 35, 'F - test_draw_arc/center:x')
  check_eq(center.y, 36, 'F - test_draw_arc/center:y')
  check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle')
  check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle')
end

function test_draw_polygon()
  io.write('\ntest_draw_polygon')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  edit.draw()
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode')
  check_eq(#Editor_state.lines, 2, 'F - test_draw_polygon/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_polygon/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_polygon/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes')
  -- first point
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_keychord('g')  -- polygon mode
  -- second point
  App.mouse_move(Editor_state.margin_left+65, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('p')  -- add point
  -- final point
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes')
  check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices')
  local shape = drawing.shapes[1]
  check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode')
  check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices')
  local p = drawing.points[shape.vertices[1]]
  check_eq(p.x, 5, 'F - test_draw_polygon/p1:x')
  check_eq(p.y, 6, 'F - test_draw_polygon/p1:y')
  local p = drawing.points[shape.vertices[2]]
  check_eq(p.x, 65, 'F - test_draw_polygon/p2:x')
  check_eq(p.y, 36, 'F - test_draw_polygon/p2:y')
  local p = drawing.points[shape.vertices[3]]
  check_eq(p.x, 35, 'F - test_draw_polygon/p3:x')
  check_eq(p.y, 26, 'F - test_draw_polygon/p3:y')
end

function test_draw_rectangle()
  io.write('\ntest_draw_rectangle')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  edit.draw()
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode')
  check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_rectangle/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes')
  -- first point
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_keychord('r')  -- rectangle mode
  -- second point/first edge
  App.mouse_move(Editor_state.margin_left+42, Editor_state.margin_top+Editor_state.drawing_padding_top+45)
  App.run_after_keychord('p')
  -- override second point/first edge
  App.mouse_move(Editor_state.margin_left+75, Editor_state.margin_top+Editor_state.drawing_padding_top+76)
  App.run_after_keychord('p')
  -- release (decides 'thickness' of rectangle perpendicular to first edge)
  App.run_after_mouse_release(Editor_state.margin_left+15, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes')
  check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points')  -- currently includes every point added
  local shape = drawing.shapes[1]
  check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode')
  check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices')
  local p = drawing.points[shape.vertices[1]]
  check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x')
  check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y')
  local p = drawing.points[shape.vertices[2]]
  check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x')
  check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y')
  local p = drawing.points[shape.vertices[3]]
  check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x')
  check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y')
  local p = drawing.points[shape.vertices[4]]
  check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x')
  check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y')
end

function test_draw_rectangle_intermediate()
  io.write('\ntest_draw_rectangle_intermediate')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  edit.draw()
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode')
  check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes')
  -- first point
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_keychord('r')  -- rectangle mode
  -- second point/first edge
  App.mouse_move(Editor_state.margin_left+42, Editor_state.margin_top+Editor_state.drawing_padding_top+45)
  App.run_after_keychord('p')
  -- override second point/first edge
  App.mouse_move(Editor_state.margin_left+75, Editor_state.margin_top+Editor_state.drawing_padding_top+76)
  App.run_after_keychord('p')
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points')  -- currently includes every point added
  local pending = drawing.pending
  check_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode')
  check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices')
  local p = drawing.points[pending.vertices[1]]
  check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x')
  check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y')
  local p = drawing.points[pending.vertices[2]]
  check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x')
  check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y')
  -- outline of rectangle is drawn based on where the mouse is, but we can't check that so far
end

function test_draw_square()
  io.write('\ntest_draw_square')
  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  edit.draw()
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode')
  check_eq(#Editor_state.lines, 2, 'F - test_draw_square/baseline/#lines')
  check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode')
  check_eq(Editor_state.lines[1].y, Editor_state.margin_top+Editor_state.drawing_padding_top, 'F - test_draw_square/baseline/y')
  check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_square/baseline/y')
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes')
  -- first point
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_keychord('s')  -- square mode
  -- second point/first edge
  App.mouse_move(Editor_state.margin_left+42, Editor_state.margin_top+Editor_state.drawing_padding_top+45)
  App.run_after_keychord('p')
  -- override second point/first edge
  App.mouse_move(Editor_state.margin_left+65, Editor_state.margin_top+Editor_state.drawing_padding_top+66)
  App.run_after_keychord('p')
  -- release (decides which side of first edge to draw square on)
  App.run_after_mouse_release(Editor_state.margin_left+15, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes')
  check_eq(#drawing.points, 5, 'F - test_draw_square/#points')  -- currently includes every point added
  check_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode')
  check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices')
  local p = drawing.points[drawing.shapes[1].vertices[1]]
  check_eq(p.x, 35, 'F - test_draw_square/p1:x')
  check_eq(p.y, 36, 'F - test_draw_square/p1:y')
  local p = drawing.points[drawing.shapes[1].vertices[2]]
  check_eq(p.x, 65, 'F - test_draw_square/p2:x')
  check_eq(p.y, 66, 'F - test_draw_square/p2:y')
  local p = drawing.points[drawing.shapes[1].vertices[3]]
  check_eq(p.x, 35, 'F - test_draw_square/p3:x')
  check_eq(p.y, 96, 'F - test_draw_square/p3:y')
  local p = drawing.points[drawing.shapes[1].vertices[4]]
  check_eq(p.x, 5, 'F - test_draw_square/p4:x')
  check_eq(p.y, 66, 'F - test_draw_square/p4:y')
end

function test_name_point()
  io.write('\ntest_name_point')
  -- create a drawing with a line
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  -- draw a line
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes')
  check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x')
  check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y')
  check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x')
  check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y')
  check_nil(p2.name, 'F - test_name_point/baseline/p2:name')
  -- enter 'name' mode without moving the mouse
  App.run_after_keychord('C-n')
  check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1')
  App.run_after_textinput('A')
  check_eq(p2.name, 'A', 'F - test_name_point')
  -- still in 'name' mode
  check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2')
  -- exit 'name' mode
  App.run_after_keychord('return')
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3')
  check_eq(p2.name, 'A', 'F - test_name_point')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- change is saved
  Editor_state.lines = load_from_disk(Editor_state.filename)
  local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
  check_eq(p2.name, 'A', 'F - test_name_point/save')
end

function test_move_point()
  io.write('\ntest_move_point')
  -- create a drawing with a line
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_move_point/baseline/#shapes')
  check_eq(#drawing.points, 2, 'F - test_move_point/baseline/#points')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point/baseline/shape:1')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_move_point/baseline/p1:x')
  check_eq(p1.y, 6, 'F - test_move_point/baseline/p1:y')
  check_eq(p2.x, 35, 'F - test_move_point/baseline/p2:x')
  check_eq(p2.y, 36, 'F - test_move_point/baseline/p2:y')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- line is saved to disk
  Editor_state.lines = load_from_disk(Editor_state.filename)
  local drawing = Editor_state.lines[1]
  local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
  check_eq(p2.x, 35, 'F - test_move_point/save/x')
  check_eq(p2.y, 36, 'F - test_move_point/save/y')
  edit.draw()
  -- enter 'move' mode without moving the mouse
  App.run_after_keychord('C-u')
  check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point/mode:1')
  -- point is lifted
  check_eq(drawing.pending.mode, 'move', 'F - test_move_point/mode:2')
  check_eq(drawing.pending.target_point, p2, 'F - test_move_point/target')
  -- move point
  App.mouse_move(Editor_state.margin_left+26, Editor_state.margin_top+Editor_state.drawing_padding_top+44)
  App.update(0.05)
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p2.x, 26, 'F - test_move_point/x')
  check_eq(p2.y, 44, 'F - test_move_point/y')
  -- exit 'move' mode
  App.run_after_mouse_click(Editor_state.margin_left+26, Editor_state.margin_top+Editor_state.drawing_padding_top+44, 1)
  check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_move_point/mode:3')
  check_eq(drawing.pending, {}, 'F - test_move_point/pending')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- change is saved
  Editor_state.lines = load_from_disk(Editor_state.filename)
  local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
  check_eq(p2.x, 26, 'F - test_move_point/save/x')
  check_eq(p2.y, 44, 'F - test_move_point/save/y')
end

function test_move_point_on_manhattan_line()
  io.write('\ntest_move_point_on_manhattan_line')
  -- create a drawing with a manhattan line
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'manhattan'
  edit.draw()
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+46, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_move_point_on_manhattan_line/baseline/#shapes')
  check_eq(#drawing.points, 2, 'F - test_move_point_on_manhattan_line/baseline/#points')
  check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_move_point_on_manhattan_line/baseline/shape:1')
  edit.draw()
  -- enter 'move' mode
  App.run_after_keychord('C-u')
  check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1')
  -- move point
  App.mouse_move(Editor_state.margin_left+26, Editor_state.margin_top+Editor_state.drawing_padding_top+44)
  App.update(0.05)
  -- line is no longer manhattan
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1')
end

function test_delete_lines_at_point()
  io.write('\ntest_delete_lines_at_point')
  -- create a drawing with two lines connected at a point
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_release(Editor_state.margin_left+55, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 2, 'F - test_delete_lines_at_point/baseline/#shapes')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:1')
  check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:2')
  -- hover on the common point and delete
  App.mouse_move(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('C-d')
  check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_lines_at_point/shape:1')
  check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_delete_lines_at_point/shape:2')
  -- wait for some time
  App.wait_fake_time(3.1)
  App.update(0)
  -- deleted points disappear after file is reloaded
  Editor_state.lines = load_from_disk(Editor_state.filename)
  check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_delete_lines_at_point/save')
end

function test_delete_line_under_mouse_pointer()
  io.write('\ntest_delete_line_under_mouse_pointer')
  -- create a drawing with two lines connected at a point
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_release(Editor_state.margin_left+55, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 2, 'F - test_delete_line_under_mouse_pointer/baseline/#shapes')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:1')
  check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:2')
  -- hover on one of the lines and delete
  App.mouse_move(Editor_state.margin_left+25, Editor_state.margin_top+Editor_state.drawing_padding_top+26)
  App.run_after_keychord('C-d')
  -- only that line is deleted
  check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_line_under_mouse_pointer/shape:1')
  check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/shape:2')
end

function test_delete_point_from_polygon()
  io.write('\ntest_delete_point_from_polygon')
  -- create a drawing with two lines connected at a point
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  -- first point
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_keychord('g')  -- polygon mode
  -- second point
  App.mouse_move(Editor_state.margin_left+65, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('p')  -- add point
  -- third point
  App.mouse_move(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+26)
  App.run_after_keychord('p')  -- add point
  -- fourth point
  App.run_after_mouse_release(Editor_state.margin_left+14, Editor_state.margin_top+Editor_state.drawing_padding_top+16, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')
  check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')
  check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices')
  -- hover on a point and delete
  App.mouse_move(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+26)
  App.run_after_keychord('C-d')
  -- just the one point is deleted
  check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape')
  check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices')
end

function test_delete_point_from_polygon()
  io.write('\ntest_delete_point_from_polygon')
  -- create a drawing with two lines connected at a point
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  -- first point
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_keychord('g')  -- polygon mode
  -- second point
  App.mouse_move(Editor_state.margin_left+65, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('p')  -- add point
  -- third point
  App.run_after_mouse_release(Editor_state.margin_left+14, Editor_state.margin_top+Editor_state.drawing_padding_top+16, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')
  check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')
  check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices')
  -- hover on a point and delete
  App.mouse_move(Editor_state.margin_left+65, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('C-d')
  -- there's < 3 points left, so the whole polygon is deleted
  check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon')
end

function test_undo_name_point()
  io.write('\ntest_undo_name_point')
  -- create a drawing with a line
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  -- draw a line
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes')
  check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x')
  check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y')
  check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x')
  check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y')
  check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name')
  check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1')
  -- enter 'name' mode without moving the mouse
  App.run_after_keychord('C-n')
  App.run_after_textinput('A')
  App.run_after_keychord('return')
  check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline')
  check_eq(#Editor_state.history, 3, 'F - test_undo_name_point/baseline/history:2')
  check_eq(Editor_state.next_history, 4, 'F - test_undo_name_point/baseline/next_history')
  -- undo
  App.run_after_keychord('C-z')
  local drawing = Editor_state.lines[1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history')
  check_eq(p2.name, '', 'F - test_undo_name_point')  -- not quite what it was before, but close enough
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- undo is saved
  Editor_state.lines = load_from_disk(Editor_state.filename)
  local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
  check_eq(p2.name, '', 'F - test_undo_name_point/save')
end

function test_undo_move_point()
  io.write('\ntest_undo_move_point')
  -- create a drawing with a line
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 1, 'F - test_undo_move_point/baseline/#shapes')
  check_eq(#drawing.points, 2, 'F - test_undo_move_point/baseline/#points')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_move_point/baseline/shape:1')
  local p1 = drawing.points[drawing.shapes[1].p1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p1.x, 5, 'F - test_undo_move_point/baseline/p1:x')
  check_eq(p1.y, 6, 'F - test_undo_move_point/baseline/p1:y')
  check_eq(p2.x, 35, 'F - test_undo_move_point/baseline/p2:x')
  check_eq(p2.y, 36, 'F - test_undo_move_point/baseline/p2:y')
  check_nil(p2.name, 'F - test_undo_move_point/baseline/p2:name')
  -- move p2
  App.run_after_keychord('C-u')
  App.mouse_move(Editor_state.margin_left+26, Editor_state.margin_top+Editor_state.drawing_padding_top+44)
  App.update(0.05)
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(p2.x, 26, 'F - test_undo_move_point/x')
  check_eq(p2.y, 44, 'F - test_undo_move_point/y')
  -- exit 'move' mode
  App.run_after_mouse_click(Editor_state.margin_left+26, Editor_state.margin_top+Editor_state.drawing_padding_top+44, 1)
  check_eq(Editor_state.next_history, 4, 'F - test_undo_move_point/next_history')
  -- undo
  App.run_after_keychord('C-z')
  App.run_after_keychord('C-z')  -- bug: need to undo twice
  local drawing = Editor_state.lines[1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(Editor_state.next_history, 2, 'F - test_undo_move_point/next_history')
  check_eq(p2.x, 35, 'F - test_undo_move_point/x')
  check_eq(p2.y, 36, 'F - test_undo_move_point/y')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- undo is saved
  Editor_state.lines = load_from_disk(Editor_state.filename)
  local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
  check_eq(p2.x, 35, 'F - test_undo_move_point/save/x')
  check_eq(p2.y, 36, 'F - test_undo_move_point/save/y')
end

function test_undo_delete_point()
  io.write('\ntest_undo_delete_point')
  -- create a drawing with two lines connected at a point
  Editor_state.filename = 'foo'
  App.screen.init{width=Editor_state.margin_width+256, height=300}  -- drawing coordinates 1:1 with pixels
  Editor_state.lines = load_array{'```lines', '```', ''}
  Editor_state.current_drawing_mode = 'line'
  edit.draw()
  App.run_after_mouse_press(Editor_state.margin_left+5, Editor_state.margin_top+Editor_state.drawing_padding_top+6, 1)
  App.run_after_mouse_release(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_press(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36, 1)
  App.run_after_mouse_release(Editor_state.margin_left+55, Editor_state.margin_top+Editor_state.drawing_padding_top+26, 1)
  local drawing = Editor_state.lines[1]
  check_eq(#drawing.shapes, 2, 'F - test_undo_delete_point/baseline/#shapes')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/baseline/shape:1')
  check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/baseline/shape:2')
  -- hover on the common point and delete
  App.mouse_move(Editor_state.margin_left+35, Editor_state.margin_top+Editor_state.drawing_padding_top+36)
  App.run_after_keychord('C-d')
  check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_undo_delete_point/shape:1')
  check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_undo_delete_point/shape:2')
  -- undo
  App.run_after_keychord('C-z')
  local drawing = Editor_state.lines[1]
  local p2 = drawing.points[drawing.shapes[1].p2]
  check_eq(Editor_state.next_history, 3, 'F - test_undo_move_point/next_history')
  check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/shape:1')
  check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/shape:2')
  -- wait until save
  App.wait_fake_time(3.1)
  App.update(0)
  -- undo is saved
  Editor_state.lines = load_from_disk(Editor_state.filename)
  check_eq(#Editor_state.lines[1].shapes, 2, 'F - test_undo_delete_point/save')
end
pre>

             
 
          















































                                                                                






                                                                                             
                                                                     
 
                                           




                                                                  
                                                                      
                                                                     
                                               







                                                                               


                                              
                                          





                                                                                        
                                                                        



                           
                              

                   





                                                      
                                                               




                                                             
                                             
                
                              

         
                         
                                      























                                                                                   
                                 





                 
                                                                    
 

                                                      
 
                                                       


                                


             
 
          
                                                                            



                      
                                             
                                                        


                 
                           
                                                                
                                                                         


                                                                             
 

                                                 
                                                       
                    


                                             
                                  

                                                           


                                                     
                                                    
                                                   
 
         

                                        
            

                                                                         
                                                       
 


                                                                                          
                

                              
 
                                                                             

                                     

                                                             
                                                                         
                    
                                                                                   







                                                                                          
                                                 
                                                                              
                    
                                                                             

                                       
                                                                                  
                                                                              
                        
                                                          
                                                                                                   
                            
                                                                                                     
                     

                 
         

     


               


             































                                                                                                         


                                                                                                         

                                                        
                                                







                                      














                                                                                                     




                
          
                                           

                                                        
                                                          
                                                                     
                                                                     
 

                                     



                                                                   
 
                                

                                                             

                              
            
                                                                         
                                    







                                                                           
 
                                                       
 
                                                                                          

                                                      
            
                              
 
                                        
                                                                                          




                                                      
                                                                 
                                                                                                             
             
                                                                

                                                                                      
             
 



                                             
                        
                                                              

                                                                                                    
                                                               

                                                                             
                                                                 

                                                                               

                                                                        
         
     
 

             


















































                                                                      
/*
 * jabber.c
 *
 * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
 *
 * This file is part of Profanity.
 *
 * Profanity is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Profanity 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <string.h>
#include <stdlib.h>
#include <assert.h>

#include <strophe.h>

#include "capabilities.h"
#include "chat_session.h"
#include "common.h"
#include "contact_list.h"
#include "jabber.h"
#include "jid.h"
#include "log.h"
#include "preferences.h"
#include "profanity.h"
#include "muc.h"
#include "stanza.h"

static struct _jabber_conn_t {
    xmpp_log_t *log;
    xmpp_ctx_t *ctx;
    xmpp_conn_t *conn;
    jabber_conn_status_t conn_status;
    jabber_presence_t presence;
    char *status;
    int tls_disabled;
    int priority;
} jabber_conn;

static GHashTable *sub_requests;

// for auto reconnect
static struct {
    char *account;
    char *jid;
    char *passwd;
    char *altdomain;
} saved_user;

static GTimer *reconnect_timer;

static log_level_t _get_log_level(xmpp_log_level_t xmpp_level);
static xmpp_log_level_t _get_xmpp_log_level();
static void _xmpp_file_logger(void * const userdata,
    const xmpp_log_level_t level, const char * const area,
    const char * const msg);
static xmpp_log_t * _xmpp_get_file_logger();

static void _jabber_roster_request(void);

// XMPP event handlers
static void _connection_handler(xmpp_conn_t * const conn,
    const xmpp_conn_event_t status, const int error,
    xmpp_stream_error_t * const stream_error, void * const userdata);

static int _message_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata);
static int _groupchat_message_handler(xmpp_stanza_t * const stanza);
static int _error_handler(xmpp_stanza_t * const stanza);
static int _chat_message_handler(xmpp_stanza_t * const stanza);

static int _iq_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata);
static int _roster_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata);
static int _disco_response_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata);
static int _disco_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata);
static int _version_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata);
static int _presence_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata);
static int _ping_timed_handler(xmpp_conn_t * const conn, void * const userdata);
static char * _handle_presence_caps(xmpp_stanza_t * const stanza);

void
jabber_init(const int disable_tls)
{
    log_info("Initialising XMPP");
    jabber_conn.conn_status = JABBER_STARTED;
    jabber_conn.presence = PRESENCE_OFFLINE;
    jabber_conn.status = NULL;
    jabber_conn.tls_disabled = disable_tls;
    sub_requests = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
}

void
jabber_restart(void)
{
    jabber_conn.conn_status = JABBER_STARTED;
    jabber_conn.presence = PRESENCE_OFFLINE;
    FREE_SET_NULL(jabber_conn.status);
}

jabber_conn_status_t
jabber_connect_with_account(ProfAccount *account, const char * const passwd)
{
    FREE_SET_NULL(saved_user.account);

    if (account->name == NULL)
        return JABBER_UNDEFINED;

    saved_user.account = strdup(account->name);
    log_info("Connecting with account: %s", account->name);
    return jabber_connect(account->jid, passwd, account->server);
}

jabber_conn_status_t
jabber_connect(const char * const jid,
    const char * const passwd, const char * const altdomain)
{
    FREE_SET_NULL(saved_user.jid);
    FREE_SET_NULL(saved_user.passwd);
    FREE_SET_NULL(saved_user.altdomain);

    if (jid == NULL || passwd == NULL)
        return JABBER_UNDEFINED;

    saved_user.jid = strdup(jid);
    saved_user.passwd = strdup(passwd);
    if (altdomain != NULL)
        saved_user.altdomain = strdup(altdomain);

    log_info("Connecting as %s", jid);
    xmpp_initialize();

    jabber_conn.log = _xmpp_get_file_logger();
    jabber_conn.ctx = xmpp_ctx_new(NULL, jabber_conn.log);
    jabber_conn.conn = xmpp_conn_new(jabber_conn.ctx);

    xmpp_conn_set_jid(jabber_conn.conn, jid);
    xmpp_conn_set_pass(jabber_conn.conn, passwd);

    if (jabber_conn.tls_disabled)
        xmpp_conn_disable_tls(jabber_conn.conn);

    int connect_status = xmpp_connect_client(jabber_conn.conn, altdomain, 0,
        _connection_handler, jabber_conn.ctx);

    if (connect_status == 0)
        jabber_conn.conn_status = JABBER_CONNECTING;
    else
        jabber_conn.conn_status = JABBER_DISCONNECTED;

    return jabber_conn.conn_status;
}

void
jabber_disconnect(void)
{
    // if connected, send end stream and wait for response
    if (jabber_conn.conn_status == JABBER_CONNECTED) {
        log_info("Closing connection");
        jabber_conn.conn_status = JABBER_DISCONNECTING;
        xmpp_disconnect(jabber_conn.conn);

        while (jabber_get_connection_status() == JABBER_DISCONNECTING) {
            jabber_process_events();
        }
        jabber_free_resources();
    }
}

void
jabber_process_events(void)
{
    // run xmpp event loop if connected, connecting or disconnecting
    if (jabber_conn.conn_status == JABBER_CONNECTED
            || jabber_conn.conn_status == JABBER_CONNECTING
            || jabber_conn.conn_status == JABBER_DISCONNECTING) {
        xmpp_run_once(jabber_conn.ctx, 10);

    // check timer and reconnect if disconnected and timer set
    } else if (prefs_get_reconnect() != 0) {
        if ((jabber_conn.conn_status == JABBER_DISCONNECTED) &&
            (reconnect_timer != NULL)) {
            if (g_timer_elapsed(reconnect_timer, NULL) > prefs_get_reconnect()) {
                log_debug("Attempting reconnect as %s", saved_user.jid);
                jabber_connect(saved_user.jid, saved_user.passwd, saved_user.altdomain);
            }
        }
    }

}

void
jabber_send(const char * const msg, const char * const recipient)
{
    if (prefs_get_states()) {
        if (!chat_session_exists(recipient)) {
            chat_session_start(recipient, TRUE);
        }
    }

    xmpp_stanza_t *message;
    if (prefs_get_states() && chat_session_get_recipient_supports(recipient)) {
        chat_session_set_active(recipient);
        message = stanza_create_message(jabber_conn.ctx, recipient, STANZA_TYPE_CHAT,
            msg, STANZA_NAME_ACTIVE);
    } else {
        message = stanza_create_message(jabber_conn.ctx, recipient, STANZA_TYPE_CHAT,
            msg, NULL);
    }

    xmpp_send(jabber_conn.conn, message);
    xmpp_stanza_release(message);
}

void
jabber_send_groupchat(const char * const msg, const char * const recipient)
{
    xmpp_stanza_t *message = stanza_create_message(jabber_conn.ctx, recipient,
        STANZA_TYPE_GROUPCHAT, msg, NULL);

    xmpp_send(jabber_conn.conn, message);
    xmpp_stanza_release(message);
}

void
jabber_send_composing(const char * const recipient)
{
    xmpp_stanza_t *stanza = stanza_create_chat_state(jabber_conn.ctx, recipient,
        STANZA_NAME_COMPOSING);

    xmpp_send(jabber_conn.conn, stanza);
    xmpp_stanza_release(stanza);
    chat_session_set_sent(recipient);
}

void
jabber_send_paused(const char * const recipient)
{
    xmpp_stanza_t *stanza = stanza_create_chat_state(jabber_conn.ctx, recipient,
        STANZA_NAME_PAUSED);

    xmpp_send(jabber_conn.conn, stanza);
    xmpp_stanza_release(stanza);
    chat_session_set_sent(recipient);
}

void
jabber_send_inactive(const char * const recipient)
{
    xmpp_stanza_t *stanza = stanza_create_chat_state(jabber_conn.ctx, recipient,
        STANZA_NAME_INACTIVE);

    xmpp_send(jabber_conn.conn, stanza);
    xmpp_stanza_release(stanza);
    chat_session_set_sent(recipient);
}

void
jabber_send_gone(const char * const recipient)
{
    xmpp_stanza_t *stanza = stanza_create_chat_state(jabber_conn.ctx, recipient,
        STANZA_NAME_GONE);

    xmpp_send(jabber_conn.conn, stanza);
    xmpp_stanza_release(stanza);
    chat_session_set_sent(recipient);
}

void
jabber_subscription(const char * const jid, jabber_subscr_t action)
{
    xmpp_stanza_t *presence;
    char *type, *jid_cpy, *bare_jid;

    // jid must be a bare JID
    jid_cpy = strdup(jid);
    bare_jid = strtok(jid_cpy, "/");
    g_hash_table_remove(sub_requests, bare_jid);

    if (action == PRESENCE_SUBSCRIBE)
        type = STANZA_TYPE_SUBSCRIBE;
    else if (action == PRESENCE_SUBSCRIBED)
        type = STANZA_TYPE_SUBSCRIBED;
    else if (action == PRESENCE_UNSUBSCRIBED)
        type = STANZA_TYPE_UNSUBSCRIBED;
    else { // unknown action
        free(jid_cpy);
        return;
    }

    presence = xmpp_stanza_new(jabber_conn.ctx);
    xmpp_stanza_set_name(presence, STANZA_NAME_PRESENCE);
    xmpp_stanza_set_type(presence, type);
    xmpp_stanza_set_attribute(presence, STANZA_ATTR_TO, bare_jid);
    xmpp_send(jabber_conn.conn, presence);
    xmpp_stanza_release(presence);
    free(jid_cpy);
}

GList *
jabber_get_subscription_requests(void)
{
    return g_hash_table_get_keys(sub_requests);
}

void
jabber_join(Jid *jid)
{
    xmpp_stanza_t *presence = stanza_create_room_join_presence(jabber_conn.ctx,
        jid->fulljid);
    xmpp_send(jabber_conn.conn, presence);
    xmpp_stanza_release(presence);

    muc_join_room(jid->barejid, jid->resourcepart);
}

void
jabber_change_room_nick(const char * const room, const char * const nick)
{
    char *full_room_jid = create_full_room_jid(room, nick);
    xmpp_stanza_t *presence = stanza_create_room_newnick_presence(jabber_conn.ctx,
        full_room_jid);
    xmpp_send(jabber_conn.conn, presence);
    xmpp_stanza_release(presence);

    free(full_room_jid);
}

void
jabber_leave_chat_room(const char * const room_jid)
{
    char *nick = muc_get_room_nick(room_jid);

    xmpp_stanza_t *presence = stanza_create_room_leave_presence(jabber_conn.ctx,
        room_jid, nick);
    xmpp_send(jabber_conn.conn, presence);
    xmpp_stanza_release(presence);
}

void
jabber_update_presence(jabber_presence_t status, const char * const msg,
    int idle)
{
    int pri;
    char *show;

    // don't send presence when disconnected
    if (jabber_conn.conn_status != JABBER_CONNECTED)
        return;

    pri = prefs_get_priority();
    if (pri < JABBER_PRIORITY_MIN || pri > JABBER_PRIORITY_MAX)
        pri = 0;

    jabber_conn.presence = status;
    jabber_conn.priority = pri;

    switch(status)
    {
        case PRESENCE_AWAY:
            show = STANZA_TEXT_AWAY;
            break;
        case PRESENCE_DND:
            show = STANZA_TEXT_DND;
            break;
        case PRESENCE_CHAT:
            show = STANZA_TEXT_CHAT;
            break;
        case PRESENCE_XA:
            show = STANZA_TEXT_XA;
            break;
        default: // PRESENCE_ONLINE
            show = NULL;
            break;
    }

    if (jabber_conn.status != NULL) {
        free(jabber_conn.status);
        jabber_conn.status = NULL;
    }
    if (msg != NULL)
        jabber_conn.status = strdup(msg);

    xmpp_stanza_t *presence = stanza_create_presence(jabber_conn.ctx, show, msg);
    if (pri != 0) {
        xmpp_stanza_t *priority, *value;
        char pri_str[10];

        snprintf(pri_str, sizeof(pri_str), "%d", pri);
        priority = xmpp_stanza_new(jabber_conn.ctx);
        value = xmpp_stanza_new(jabber_conn.ctx);
        xmpp_stanza_set_name(priority, STANZA_NAME_PRIORITY);
        xmpp_stanza_set_text(value, pri_str);
        xmpp_stanza_add_child(priority, value);
        xmpp_stanza_add_child(presence, priority);
    }

    if (idle > 0) {
        xmpp_stanza_t *query = xmpp_stanza_new(jabber_conn.ctx);
        xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
        xmpp_stanza_set_ns(query, STANZA_NS_LASTACTIVITY);
        char idle_str[10];
        snprintf(idle_str, sizeof(idle_str), "%d", idle);
        xmpp_stanza_set_attribute(query, STANZA_ATTR_SECONDS, idle_str);
        xmpp_stanza_add_child(presence, query);
    }

    // add caps
    xmpp_stanza_t *caps = xmpp_stanza_new(jabber_conn.ctx);
    xmpp_stanza_set_name(caps, STANZA_NAME_C);
    xmpp_stanza_set_ns(caps, STANZA_NS_CAPS);
    xmpp_stanza_set_attribute(caps, STANZA_ATTR_HASH, "sha-1");
    xmpp_stanza_set_attribute(caps, STANZA_ATTR_NODE, "http://www.profanity.im");
    xmpp_stanza_t *query = caps_create_query_response_stanza(jabber_conn.ctx);
    char *sha1 = caps_create_sha1_str(query);
    xmpp_stanza_set_attribute(caps, STANZA_ATTR_VER, sha1);
    xmpp_stanza_add_child(presence, caps);
    xmpp_send(jabber_conn.conn, presence);

    // send presence for each room
    GList *rooms = muc_get_active_room_list();
    while (rooms != NULL) {
        char *room = rooms->data;
        char *nick = muc_get_room_nick(room);
        char *full_room_jid = create_full_room_jid(room, nick);

        xmpp_stanza_set_attribute(presence, STANZA_ATTR_TO, full_room_jid);
        xmpp_send(jabber_conn.conn, presence);

        rooms = g_list_next(rooms);
    }
    g_list_free(rooms);

    xmpp_stanza_release(presence);
}

void
jabber_set_autoping(int seconds)
{
    if (jabber_conn.conn_status == JABBER_CONNECTED) {
        xmpp_timed_handler_delete(jabber_conn.conn, _ping_timed_handler);

        if (seconds != 0) {
            int millis = seconds * 1000;
            xmpp_timed_handler_add(jabber_conn.conn, _ping_timed_handler, millis,
                jabber_conn.ctx);
        }
    }
}

jabber_conn_status_t
jabber_get_connection_status(void)
{
    return (jabber_conn.conn_status);
}

const char *
jabber_get_jid(void)
{
    return xmpp_conn_get_jid(jabber_conn.conn);
}

int
jabber_get_priority(void)
{
    return jabber_conn.priority;
}

jabber_presence_t
jabber_get_presence(void)
{
    return jabber_conn.presence;
}

char *
jabber_get_status(void)
{
    if (jabber_conn.status == NULL)
        return NULL;
    else
        return strdup(jabber_conn.status);
}

void
jabber_free_resources(void)
{
    FREE_SET_NULL(saved_user.jid);
    FREE_SET_NULL(saved_user.passwd);
    FREE_SET_NULL(saved_user.account);
    FREE_SET_NULL(saved_user.altdomain);
    chat_sessions_clear();
    if (sub_requests != NULL)
        g_hash_table_remove_all(sub_requests);
    xmpp_conn_release(jabber_conn.conn);
    xmpp_ctx_free(jabber_conn.ctx);
    xmpp_shutdown();
}

static void
_jabber_roster_request(void)
{
    xmpp_stanza_t *iq = stanza_create_roster_iq(jabber_conn.ctx);
    xmpp_send(jabber_conn.conn, iq);
    xmpp_stanza_release(iq);
}

static int
_message_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata)
{
    gchar *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);

    if (type == NULL) {
        log_error("Message stanza received with no type attribute");
        return 1;
    } else if (strcmp(type, STANZA_TYPE_ERROR) == 0) {
        return _error_handler(stanza);
    } else if (strcmp(type, STANZA_TYPE_GROUPCHAT) == 0) {
        return _groupchat_message_handler(stanza);
    } else if (strcmp(type, STANZA_TYPE_CHAT) == 0) {
        return _chat_message_handler(stanza);
    } else {
        log_error("Message stanza received with unknown type: %s", type);
        return 1;
    }
}

static int
_groupchat_message_handler(xmpp_stanza_t * const stanza)
{
    char *message = NULL;
    char *room_jid = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
    Jid *jid = jid_create(room_jid);

    // handle room broadcasts
    if (jid->resourcepart == NULL) {
        xmpp_stanza_t *subject = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_SUBJECT);

        // handle subject
        if (subject != NULL) {
            message = xmpp_stanza_get_text(subject);
            if (message != NULL) {
                prof_handle_room_subject(jid->barejid, message);
            }

            return 1;

        // handle other room broadcasts
        } else {
            xmpp_stanza_t *body = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_BODY);
            if (body != NULL) {
                message = xmpp_stanza_get_text(body);
                if (message != NULL) {
                    prof_handle_room_broadcast(room_jid, message);
                }
            }

            return 1;
        }
    }


    if (!jid_is_valid_room_form(jid)) {
        log_error("Invalid room JID: %s", jid->str);
        return 1;
    }

    // room not active in profanity
    if (!muc_room_is_active(jid)) {
        log_error("Message recieved for inactive chat room: %s", jid->str);
        return 1;
    }

    // determine if the notifications happened whilst offline
    GTimeVal tv_stamp;
    gboolean delayed = stanza_get_delay(stanza, &tv_stamp);
    xmpp_stanza_t *body = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_BODY);

    // check for and deal with message
    if (body != NULL) {
        char *message = xmpp_stanza_get_text(body);
        if (delayed) {
            prof_handle_room_history(jid->barejid, jid->resourcepart, tv_stamp, message);
        } else {
            prof_handle_room_message(jid->barejid, jid->resourcepart, message);
        }
    }

    jid_destroy(jid);

    return 1;
}

static int
_error_handler(xmpp_stanza_t * const stanza)
{
    gchar *err_msg = NULL;
    gchar *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
    xmpp_stanza_t *error_stanza = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_ERROR);
        xmpp_stanza_t *text_stanza =
            xmpp_stanza_get_child_by_name(error_stanza, STANZA_NAME_TEXT);

    if (error_stanza == NULL) {
        log_debug("error message without <error/> received");
    } else {

        // check for text
        if (text_stanza != NULL) {
            err_msg = xmpp_stanza_get_text(text_stanza);
            prof_handle_error_message(from, err_msg);

            // TODO : process 'type' attribute from <error/> [RFC6120, 8.3.2]

        // otherwise show defined-condition
        } else {
            xmpp_stanza_t *err_cond = xmpp_stanza_get_children(error_stanza);

            if (err_cond == NULL) {
                log_debug("error message without <defined-condition/> or <text/> received");

            } else {
                err_msg = xmpp_stanza_get_name(err_cond);
                prof_handle_error_message(from, err_msg);

                // TODO : process 'type' attribute from <error/> [RFC6120, 8.3.2]
            }
        }
    }

    return 1;
}

static int
_chat_message_handler(xmpp_stanza_t * const stanza)
{
    gchar *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
    Jid *jid = jid_create(from);

    // private message from chat room use full jid (room/nick)
    if (muc_room_is_active(jid)) {
        // determine if the notifications happened whilst offline
        GTimeVal tv_stamp;
        gboolean delayed = stanza_get_delay(stanza, &tv_stamp);

        // check for and deal with message
        xmpp_stanza_t *body = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_BODY);
        if (body != NULL) {
            char *message = xmpp_stanza_get_text(body);
            if (delayed) {
                prof_handle_delayed_message(jid->str, message, tv_stamp, TRUE);
            } else {
                prof_handle_incoming_message(jid->str, message, TRUE);
            }
        }

        free(jid);
        return 1;

    // standard chat message, use jid without resource
    } else {
        // determine chatstate support of recipient
        gboolean recipient_supports = FALSE;
        if (stanza_contains_chat_state(stanza)) {
            recipient_supports = TRUE;
        }

        // create or update chat session
        if (!chat_session_exists(jid->barejid)) {
            chat_session_start(jid->barejid, recipient_supports);
        } else {
            chat_session_set_recipient_supports(jid->barejid, recipient_supports);
        }

        // determine if the notifications happened whilst offline
        GTimeVal tv_stamp;
        gboolean delayed = stanza_get_delay(stanza, &tv_stamp);

        // deal with chat states if recipient supports them
        if (recipient_supports && (!delayed)) {
            if (xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_COMPOSING) != NULL) {
                if (prefs_get_notify_typing() || prefs_get_intype()) {
                    prof_handle_typing(jid->barejid);
                }
            } else if (xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_GONE) != NULL) {
                prof_handle_gone(jid->barejid);
            } else if (xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_PAUSED) != NULL) {
                // do something
            } else if (xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_INACTIVE) != NULL) {
                // do something
            } else { // handle <active/>
                // do something
            }
        }

        // check for and deal with message
        xmpp_stanza_t *body = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_BODY);
        if (body != NULL) {
            char *message = xmpp_stanza_get_text(body);
            if (delayed) {
                prof_handle_delayed_message(jid->barejid, message, tv_stamp, FALSE);
            } else {
                prof_handle_incoming_message(jid->barejid, message, FALSE);
            }
        }

        free(jid);
        return 1;
    }

}

static void
_connection_handler(xmpp_conn_t * const conn,
    const xmpp_conn_event_t status, const int error,
    xmpp_stream_error_t * const stream_error, void * const userdata)
{
    xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata;

    // login success
    if (status == XMPP_CONN_CONNECT) {
        if (saved_user.account != NULL) {
            prof_handle_login_account_success(saved_user.account);
        } else {
            const char *jid = xmpp_conn_get_jid(conn);
            prof_handle_login_success(jid, saved_user.altdomain);
        }

        chat_sessions_init();

        xmpp_handler_add(conn, _message_handler, NULL, STANZA_NAME_MESSAGE, NULL, ctx);
        xmpp_handler_add(conn, _presence_handler, NULL, STANZA_NAME_PRESENCE, NULL, ctx);
        xmpp_handler_add(conn, _iq_handler, NULL, STANZA_NAME_IQ, NULL, ctx);

        if (prefs_get_autoping() != 0) {
            int millis = prefs_get_autoping() * 1000;
            xmpp_timed_handler_add(conn, _ping_timed_handler, millis, ctx);
        }

        _jabber_roster_request();
        jabber_conn.conn_status = JABBER_CONNECTED;
        jabber_conn.presence = PRESENCE_ONLINE;

        if (prefs_get_reconnect() != 0) {
            if (reconnect_timer != NULL) {
                g_timer_destroy(reconnect_timer);
                reconnect_timer = NULL;
            }
        }

    } else if (status == XMPP_CONN_DISCONNECT) {

        // lost connection for unkown reason
        if (jabber_conn.conn_status == JABBER_CONNECTED) {
            prof_handle_lost_connection();
            if (prefs_get_reconnect() != 0) {
                assert(reconnect_timer == NULL);
                reconnect_timer = g_timer_new();
                // TODO: free resources but leave saved_user untouched
            } else {
                jabber_free_resources();
            }

        // login attempt failed
        } else if (jabber_conn.conn_status != JABBER_DISCONNECTING) {
            if (reconnect_timer == NULL) {
                prof_handle_failed_login();
                jabber_free_resources();
            } else {
                if (prefs_get_reconnect() != 0) {
                    g_timer_start(reconnect_timer);
                }
                // TODO: free resources but leave saved_user untouched
            }
        }

        // close stream response from server after disconnect is handled too
        jabber_conn.conn_status = JABBER_DISCONNECTED;
        jabber_conn.presence = PRESENCE_OFFLINE;
    }
}

static int
_iq_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata)
{
    char *id = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_ID);
    char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);

    // handle the initial roster request
    if (g_strcmp0(id, "roster") == 0) {
        return _roster_handler(conn, stanza, userdata);

    // handle disco responses
    } else if ((id != NULL) && (g_str_has_prefix(id, "disco")) &&
            (g_strcmp0(type, "result") == 0)) {
        return _disco_response_handler(conn, stanza, userdata);

    // handle disco requests
    } else if (stanza_is_caps_request(stanza)) {
        return _disco_request_handler(conn, stanza, userdata);

    } else if (stanza_is_version_request(stanza)) {
        return _version_request_handler(conn, stanza, userdata);

    // handle iq
    } else {
        char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
        if (type == NULL) {
            return TRUE;
        }

        // handle roster update
        if (strcmp(type, STANZA_TYPE_SET) == 0) {

            xmpp_stanza_t *query =
                xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
            if (query == NULL) {
                return TRUE;
            }

            char *xmlns = xmpp_stanza_get_attribute(query, STANZA_ATTR_XMLNS);
            if (xmlns == NULL) {
                return TRUE;
            }
            if (strcmp(xmlns, XMPP_NS_ROSTER) != 0) {
                return TRUE;
            }

            xmpp_stanza_t *item =
                xmpp_stanza_get_child_by_name(query, STANZA_NAME_ITEM);
            if (item == NULL) {
                return TRUE;
            }

            const char *jid = xmpp_stanza_get_attribute(item, STANZA_ATTR_JID);
            const char *sub = xmpp_stanza_get_attribute(item, STANZA_ATTR_SUBSCRIPTION);
            if (g_strcmp0(sub, "remove") == 0) {
                contact_list_remove(jid);
                return TRUE;
            }

            gboolean pending_out = FALSE;
            const char *ask = xmpp_stanza_get_attribute(item, STANZA_ATTR_ASK);
            if ((ask != NULL) && (strcmp(ask, "subscribe") == 0)) {
                pending_out = TRUE;
            }

            contact_list_update_subscription(jid, sub, pending_out);

            return TRUE;

        // handle server ping
        } else if (strcmp(type, STANZA_TYPE_GET) == 0) {
            xmpp_stanza_t *ping = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_PING);
            if (ping == NULL) {
                return TRUE;
            }

            char *xmlns = xmpp_stanza_get_attribute(ping, STANZA_ATTR_XMLNS);
            if (xmlns == NULL) {
                return TRUE;
            }

            if (strcmp(xmlns, STANZA_NS_PING) != 0) {
                return TRUE;
            }

            char *to = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TO);
            char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
            if ((from == NULL) || (to == NULL)) {
                return TRUE;
            }

            xmpp_stanza_t *pong = xmpp_stanza_new(jabber_conn.ctx);
            xmpp_stanza_set_name(pong, STANZA_NAME_IQ);
            xmpp_stanza_set_attribute(pong, STANZA_ATTR_TO, from);
            xmpp_stanza_set_attribute(pong, STANZA_ATTR_FROM, to);
            xmpp_stanza_set_attribute(pong, STANZA_ATTR_TYPE, STANZA_TYPE_RESULT);
            if (id != NULL) {
                xmpp_stanza_set_attribute(pong, STANZA_ATTR_ID, id);
            }

            xmpp_send(jabber_conn.conn, pong);
            xmpp_stanza_release(pong);

            return TRUE;
        } else {
            return TRUE;
        }
    }
}

static int
_roster_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata)
{
    xmpp_stanza_t *query, *item;
    char *type = xmpp_stanza_get_type(stanza);

    if (strcmp(type, STANZA_TYPE_ERROR) == 0)
        log_error("Roster query failed");
    else {
        query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
        item = xmpp_stanza_get_children(query);

        while (item != NULL) {
            const char *jid = xmpp_stanza_get_attribute(item, STANZA_ATTR_JID);
            const char *name = xmpp_stanza_get_attribute(item, STANZA_ATTR_NAME);
            const char *sub = xmpp_stanza_get_attribute(item, STANZA_ATTR_SUBSCRIPTION);

            gboolean pending_out = FALSE;
            const char *ask = xmpp_stanza_get_attribute(item, STANZA_ATTR_ASK);
            if ((ask != NULL) && (strcmp(ask, "subscribe") == 0)) {
                pending_out = TRUE;
            }

            gboolean added = contact_list_add(jid, name, "offline", NULL, sub,
                pending_out);

            if (!added) {
                log_warning("Attempt to add contact twice: %s", jid);
            }

            item = xmpp_stanza_get_next(item);
        }

        /* TODO: Save somehow last presence show and use it for initial
         *       presence rather than PRESENCE_ONLINE. It will be helpful
         *       when I set dnd status and reconnect for some reason */
        // send initial presence
        jabber_update_presence(PRESENCE_ONLINE, NULL, 0);
    }

    return 1;
}

static int
_version_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata)
{
    xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata;

    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
    char *id = xmpp_stanza_get_id(stanza);

    if (from != NULL) {
        xmpp_stanza_t *response = xmpp_stanza_new(ctx);
        xmpp_stanza_set_name(response, STANZA_NAME_IQ);
        if (id != NULL) {
            xmpp_stanza_set_id(response, id);
        }
        xmpp_stanza_set_attribute(response, STANZA_ATTR_TO, from);
        xmpp_stanza_set_type(response, STANZA_TYPE_RESULT);

        xmpp_stanza_t *query = xmpp_stanza_new(ctx);
        xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
        xmpp_stanza_set_ns(query, STANZA_NS_VERSION);

        xmpp_stanza_t *name = xmpp_stanza_new(ctx);
        xmpp_stanza_set_name(name, "name");
        xmpp_stanza_t *name_txt = xmpp_stanza_new(ctx);
        xmpp_stanza_set_text(name_txt, "Profanity");
        xmpp_stanza_add_child(name, name_txt);

        xmpp_stanza_t *version = xmpp_stanza_new(ctx);
        xmpp_stanza_set_name(version, "version");
        xmpp_stanza_t *version_txt = xmpp_stanza_new(ctx);
        GString *version_str = g_string_new(PACKAGE_VERSION);
        if (strcmp(PACKAGE_STATUS, "development") == 0) {
            g_string_append(version_str, "dev");
        }
        xmpp_stanza_set_text(version_txt, version_str->str);
        xmpp_stanza_add_child(version, version_txt);

        xmpp_stanza_add_child(query, name);
        xmpp_stanza_add_child(query, version);
        xmpp_stanza_add_child(response, query);

        xmpp_send(conn, response);
    }

    return 1;
}

static int
_disco_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata)
{
    xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata;

    xmpp_stanza_t *incoming_query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
    char *node_str = xmpp_stanza_get_attribute(incoming_query, STANZA_ATTR_NODE);
    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);

    if (from != NULL && node_str != NULL) {
        xmpp_stanza_t *response = xmpp_stanza_new(ctx);
        xmpp_stanza_set_name(response, STANZA_NAME_IQ);
        xmpp_stanza_set_id(response, xmpp_stanza_get_id(stanza));
        xmpp_stanza_set_attribute(response, STANZA_ATTR_TO, from);
        xmpp_stanza_set_type(response, STANZA_TYPE_RESULT);
        xmpp_stanza_t *query = caps_create_query_response_stanza(ctx);
        xmpp_stanza_set_attribute(query, STANZA_ATTR_NODE, node_str);
        xmpp_stanza_add_child(response, query);
        xmpp_send(conn, response);
    }

    return 1;
}

static int
_disco_response_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
    void * const userdata)
{
    char *type = xmpp_stanza_get_type(stanza);
    char *id = xmpp_stanza_get_id(stanza);

    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
        log_error("Roster query failed");
        return 1;
    } else {
        xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
        char *node = xmpp_stanza_get_attribute(query, STANZA_ATTR_NODE);
        if (node == NULL) {
            return 1;
        }

        char *caps_key = NULL;

        // xep-0115
        if (g_strcmp0(id, "disco") == 0) {
            caps_key = node;

            // validate sha1
            gchar **split = g_strsplit(node, "#", -1);
            char *given_sha1 = split[1];
            char *generated_sha1 = caps_create_sha1_str(query);

            if (g_strcmp0(given_sha1, generated_sha1) != 0) {
                log_info("Invalid SHA1 recieved for caps.");
                return 1;
            }
        // non supported hash, or legacy caps
        } else {
            caps_key = id + 6;
        }

        // already cached
        if (caps_contains(caps_key)) {
            log_info("Client info already cached.");
            return 1;
        }

        xmpp_stanza_t *identity = xmpp_stanza_get_child_by_name(query, "identity");

        if (identity == NULL) {
            return 1;
        }

        const char *category = xmpp_stanza_get_attribute(identity, "category");
        if (category == NULL) {
            return 1;
        }

        if (strcmp(category, "client") != 0) {
            return 1;
        }

        const char *name = xmpp_stanza_get_attribute(identity, "name");
        if (name == 0) {
            return 1;
        }

        caps_add(caps_key, name);

        return 1;
    }
}

static int
_ping_timed_handler(xmpp_conn_t * const conn, void * const userdata)
{
    if (jabber_conn.conn_status == JABBER_CONNECTED) {
        xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata;

        xmpp_stanza_t *iq = stanza_create_ping_iq(ctx);
        xmpp_send(conn, iq);
        xmpp_stanza_release(iq);
    }

    return 1;
}

static int
_room_presence_handler(const char * const jid, xmpp_stanza_t * const stanza)
{
    char *room = NULL;
    char *nick = NULL;

    if (!parse_room_jid(jid, &room, &nick)) {
        log_error("Could not parse room jid: %s", room);
        return 1;
    }

    // handle self presence
    if (stanza_is_muc_self_presence(stanza, jabber_get_jid())) {
        char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
        gboolean nick_change = stanza_is_room_nick_change(stanza);

        if ((type != NULL) && (strcmp(type, STANZA_TYPE_UNAVAILABLE) == 0)) {

            // leave room if not self nick change
            if (nick_change) {
                muc_set_room_pending_nick_change(room);
            } else {
                prof_handle_leave_room(room);
            }

        // handle self nick change
        } else if (muc_is_room_pending_nick_change(room)) {
            muc_complete_room_nick_change(room, nick);
            prof_handle_room_nick_change(room, nick);

        // handle roster complete
        } else if (!muc_get_roster_received(room)) {
            prof_handle_room_roster_complete(room);

        }

    // handle presence from room members
    } else {
        char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
        char *show_str, *status_str;
        char *caps_key = _handle_presence_caps(stanza);

        xmpp_stanza_t *status = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_STATUS);
        if (status != NULL) {
            status_str = xmpp_stanza_get_text(status);
        } else {
            status_str = NULL;
        }

        if ((type != NULL) && (strcmp(type, STANZA_TYPE_UNAVAILABLE) == 0)) {

            // handle nickname change
            if (stanza_is_room_nick_change(stanza)) {
                char *new_nick = stanza_get_new_nick(stanza);
                muc_set_roster_pending_nick_change(room, new_nick, nick);
            } else {
                prof_handle_room_member_offline(room, nick, "offline", status_str);
            }
        } else {
            xmpp_stanza_t *show = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_SHOW);
            if (show != NULL) {
                show_str = xmpp_stanza_get_text(show);
            } else {
                show_str = "online";
            }
            if (!muc_get_roster_received(room)) {
                muc_add_to_roster(room, nick, show_str, status_str, caps_key);
            } else {
                char *old_nick = muc_complete_roster_nick_change(room, nick);

                if (old_nick != NULL) {
                    muc_add_to_roster(room, nick, show_str, status_str, caps_key);
                    prof_handle_room_member_nick_change(room, old_nick, nick);
                } else {
                    if (!muc_nick_in_roster(room, nick)) {
                        prof_handle_room_member_online(room, nick, show_str, status_str, caps_key);
                    } else {
                        prof_handle_room_member_presence(room, nick, show_str, status_str, caps_key);
                    }
                }
            }
        }
    }

    free(room);
    free(nick);

    return 1;
}

static char *
_handle_presence_caps(xmpp_stanza_t * const stanza)
{
    char *caps_key = NULL;
    char *node = NULL;
    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
    if (stanza_contains_caps(stanza)) {
        char *hash_type = stanza_caps_get_hash(stanza);

        // xep-0115
        if (hash_type != NULL) {

            // supported hash
            if (strcmp(hash_type, "sha-1") == 0) {
                node = stanza_get_caps_str(stanza);
                caps_key = node;

                if (node != NULL) {
                    if (!caps_contains(caps_key)) {
                        xmpp_stanza_t *iq = stanza_create_disco_iq(jabber_conn.ctx, "disco", from, node);
                        xmpp_send(jabber_conn.conn, iq);
                        xmpp_stanza_release(iq);
                    }
                }

            // unsupported hash
            } else {
                node = stanza_get_caps_str(stanza);
                caps_key = from;

                if (node != NULL) {
                    if (!caps_contains(caps_key)) {
                        GString *id = g_string_new("disco_");
                        g_string_append(id, from);
                        xmpp_stanza_t *iq = stanza_create_disco_iq(jabber_conn.ctx, id->str, from, node);
                        xmpp_send(jabber_conn.conn, iq);
                        xmpp_stanza_release(iq);
                        g_string_free(id, TRUE);
                    }
                }
            }

            return strdup(caps_key);

        //ignore or handle legacy caps
        } else {
            node = stanza_get_caps_str(stanza);
            caps_key = from;

            if (node != NULL) {
                if (!caps_contains(caps_key)) {
                    GString *id = g_string_new("disco_");
                    g_string_append(id, from);
                    xmpp_stanza_t *iq = stanza_create_disco_iq(jabber_conn.ctx, id->str, from, node);
                    xmpp_send(jabber_conn.conn, iq);
                    xmpp_stanza_release(iq);
                    g_string_free(id, TRUE);
                }
            }

            return caps_key;
        }
    }
    return NULL;
}

static int
_presence_handler(xmpp_conn_t * const conn,
    xmpp_stanza_t * const stanza, void * const userdata)
{
    const char *jid = xmpp_conn_get_jid(jabber_conn.conn);
    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
    char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);

    Jid *my_jid = jid_create(jid);
    Jid *from_jid = jid_create(from);

    if ((type != NULL) && (strcmp(type, STANZA_TYPE_ERROR) == 0)) {
        return _error_handler(stanza);
    }

    // handle chat room presence
    if (muc_room_is_active(from_jid)) {
        return _room_presence_handler(from_jid->str, stanza);

    // handle regular presence
    } else {
        char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
        char *show_str, *status_str;
        int idle_seconds = stanza_get_idle_time(stanza);
        GDateTime *last_activity = NULL;

        if (idle_seconds > 0) {
            GDateTime *now = g_date_time_new_now_local();
            last_activity = g_date_time_add_seconds(now, 0 - idle_seconds);
            g_date_time_unref(now);
        }

        char *caps_key = _handle_presence_caps(stanza);

        xmpp_stanza_t *status = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_STATUS);
        if (status != NULL)
            status_str = xmpp_stanza_get_text(status);
        else
            status_str = NULL;

        if (type == NULL) { // available
            xmpp_stanza_t *show = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_SHOW);
            if (show != NULL)
                show_str = xmpp_stanza_get_text(show);
            else
                show_str = "online";

            if (strcmp(my_jid->barejid, from_jid->barejid) !=0) {
                prof_handle_contact_online(from_jid->barejid, show_str, status_str, last_activity, caps_key);
            }
        } else if (strcmp(type, STANZA_TYPE_UNAVAILABLE) == 0) {
            if (strcmp(my_jid->barejid, from_jid->barejid) !=0) {
                prof_handle_contact_offline(from_jid->barejid, "offline", status_str);
            }

        if (last_activity != NULL) {
            g_date_time_unref(last_activity);
        }

        // subscriptions
        } else if (strcmp(type, STANZA_TYPE_SUBSCRIBE) == 0) {
            prof_handle_subscription(from_jid->barejid, PRESENCE_SUBSCRIBE);
            g_hash_table_insert(sub_requests, strdup(from_jid->barejid), strdup(from_jid->barejid));
        } else if (strcmp(type, STANZA_TYPE_SUBSCRIBED) == 0) {
            prof_handle_subscription(from_jid->barejid, PRESENCE_SUBSCRIBED);
            g_hash_table_remove(sub_requests, from_jid->barejid);
        } else if (strcmp(type, STANZA_TYPE_UNSUBSCRIBED) == 0) {
            prof_handle_subscription(from_jid->barejid, PRESENCE_UNSUBSCRIBED);
            g_hash_table_remove(sub_requests, from_jid->barejid);
        } else { /* unknown type */
            log_debug("Received presence with unknown type '%s'", type);
        }
    }

    return 1;
}

static log_level_t
_get_log_level(xmpp_log_level_t xmpp_level)
{
    if (xmpp_level == XMPP_LEVEL_DEBUG) {
        return PROF_LEVEL_DEBUG;
    } else if (xmpp_level == XMPP_LEVEL_INFO) {
        return PROF_LEVEL_INFO;
    } else if (xmpp_level == XMPP_LEVEL_WARN) {
        return PROF_LEVEL_WARN;
    } else {
        return PROF_LEVEL_ERROR;
    }
}

static xmpp_log_level_t
_get_xmpp_log_level()
{
    log_level_t prof_level = log_get_filter();

    if (prof_level == PROF_LEVEL_DEBUG) {
        return XMPP_LEVEL_DEBUG;
    } else if (prof_level == PROF_LEVEL_INFO) {
        return XMPP_LEVEL_INFO;
    } else if (prof_level == PROF_LEVEL_WARN) {
        return XMPP_LEVEL_WARN;
    } else {
        return XMPP_LEVEL_ERROR;
    }
}

static void
_xmpp_file_logger(void * const userdata, const xmpp_log_level_t level,
    const char * const area, const char * const msg)
{
    log_level_t prof_level = _get_log_level(level);
    log_msg(prof_level, area, msg);
}

static xmpp_log_t *
_xmpp_get_file_logger()
{
    xmpp_log_level_t level = _get_xmpp_log_level();
    xmpp_log_t *file_log = malloc(sizeof(xmpp_log_t));

    file_log->handler = _xmpp_file_logger;
    file_log->userdata = &level;

    return file_log;
}